mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
merge from local
This commit is contained in:
parent
65d75ef939
commit
a1bb256f08
@ -9,7 +9,7 @@
|
|||||||
3006 -> sync
|
3006 -> sync
|
||||||
3007 -> ems (External Messaging Service)
|
3007 -> ems (External Messaging Service)
|
||||||
3008 -> users
|
3008 -> users
|
||||||
3009 -> unallocated
|
3009 -> notifications
|
||||||
3010 -> unallocated
|
3010 -> unallocated
|
||||||
3011 -> unallocated
|
3011 -> unallocated
|
||||||
3012 -> unallocated
|
3012 -> unallocated
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
const path = require("path")
|
const path = require("path")
|
||||||
const { builtinModules } = require("module")
|
|
||||||
|
|
||||||
const aliases = {
|
const aliases = {
|
||||||
"node:buffer": "buffer",
|
"node:buffer": "buffer",
|
||||||
@ -19,7 +18,7 @@ const aliases = {
|
|||||||
hooks: path.join(__dirname, "src/hooks"),
|
hooks: path.join(__dirname, "src/hooks"),
|
||||||
classes: path.join(__dirname, "src/classes"),
|
classes: path.join(__dirname, "src/classes"),
|
||||||
"comty.js": path.join(__dirname, "../../", "comty.js", "src"),
|
"comty.js": path.join(__dirname, "../../", "comty.js", "src"),
|
||||||
models: path.join(__dirname, "../comty.js/src/models"),
|
models: path.join(__dirname, "../../", "comty.js/src/models"),
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = (config = {}) => {
|
module.exports = (config = {}) => {
|
||||||
@ -30,11 +29,6 @@ module.exports = (config = {}) => {
|
|||||||
config.server = {}
|
config.server = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// config.define = {
|
|
||||||
// "global.Uint8Array": "Uint8Array",
|
|
||||||
// "process.env.NODE_DEBUG": false,
|
|
||||||
// }
|
|
||||||
|
|
||||||
config.resolve.alias = aliases
|
config.resolve.alias = aliases
|
||||||
config.server.port = process.env.listenPort ?? 8000
|
config.server.port = process.env.listenPort ?? 8000
|
||||||
config.server.host = "0.0.0.0"
|
config.server.host = "0.0.0.0"
|
||||||
@ -56,25 +50,5 @@ module.exports = (config = {}) => {
|
|||||||
target: "esnext"
|
target: "esnext"
|
||||||
}
|
}
|
||||||
|
|
||||||
// config.build = {
|
|
||||||
// sourcemap: "inline",
|
|
||||||
// target: `node16`,
|
|
||||||
// outDir: "dist",
|
|
||||||
// assetsDir: ".",
|
|
||||||
// minify: process.env.MODE !== "development",
|
|
||||||
// rollupOptions: {
|
|
||||||
// external: [
|
|
||||||
// "electron",
|
|
||||||
// "electron-devtools-installer",
|
|
||||||
// ...builtinModules.flatMap(p => [p, `node:16`]),
|
|
||||||
// ],
|
|
||||||
// output: {
|
|
||||||
// entryFileNames: "[name].js",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// emptyOutDir: true,
|
|
||||||
// brotliSize: false,
|
|
||||||
// }
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"low_performance_mode": false,
|
||||||
|
"transcode_video_browser": false,
|
||||||
"forceMobileMode": false,
|
"forceMobileMode": false,
|
||||||
"ui.effects": true,
|
"ui.effects": true,
|
||||||
"ui.general_volume": 50,
|
"ui.general_volume": 50,
|
||||||
|
@ -82,6 +82,12 @@ export default [
|
|||||||
useLayout: "minimal",
|
useLayout: "minimal",
|
||||||
public: true
|
public: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/marketplace/*",
|
||||||
|
useLayout: "default",
|
||||||
|
centeredContent: true,
|
||||||
|
extendedContent: true,
|
||||||
|
},
|
||||||
// THIS MUST BE THE LAST ROUTE
|
// THIS MUST BE THE LAST ROUTE
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
@ -7,6 +7,8 @@ import { Icons } from "components/Icons"
|
|||||||
|
|
||||||
import config from "config"
|
import config from "config"
|
||||||
|
|
||||||
|
import LatencyIndicator from "components/PerformanceIndicators/latency"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const connectionsTooltipStrings = {
|
const connectionsTooltipStrings = {
|
||||||
@ -48,9 +50,7 @@ export default {
|
|||||||
|
|
||||||
const [serverManifest, setServerManifest] = React.useState(null)
|
const [serverManifest, setServerManifest] = React.useState(null)
|
||||||
const [serverOrigin, setServerOrigin] = React.useState(null)
|
const [serverOrigin, setServerOrigin] = React.useState(null)
|
||||||
const [serverHealth, setServerHealth] = React.useState(null)
|
|
||||||
const [secureConnection, setSecureConnection] = React.useState(false)
|
const [secureConnection, setSecureConnection] = React.useState(false)
|
||||||
const [connectionPing, setConnectionPing] = React.useState({})
|
|
||||||
const [capInfo, setCapInfo] = React.useState(null)
|
const [capInfo, setCapInfo] = React.useState(null)
|
||||||
|
|
||||||
const setCapacitorInfo = async () => {
|
const setCapacitorInfo = async () => {
|
||||||
@ -68,7 +68,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checkServerOrigin = async () => {
|
const checkServerOrigin = async () => {
|
||||||
const instance = app.cores.api.instance()
|
const instance = app.cores.api.client()
|
||||||
|
|
||||||
if (instance) {
|
if (instance) {
|
||||||
setServerOrigin(instance.mainOrigin)
|
setServerOrigin(instance.mainOrigin)
|
||||||
@ -79,29 +79,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const measurePing = async () => {
|
|
||||||
const result = await app.cores.api.measurePing()
|
|
||||||
|
|
||||||
console.log(result)
|
|
||||||
|
|
||||||
setConnectionPing(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
checkServerVersion()
|
checkServerVersion()
|
||||||
checkServerOrigin()
|
checkServerOrigin()
|
||||||
|
|
||||||
measurePing()
|
|
||||||
|
|
||||||
setCapacitorInfo()
|
setCapacitorInfo()
|
||||||
|
|
||||||
const measureInterval = setInterval(() => {
|
|
||||||
measurePing()
|
|
||||||
}, 3000)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(measureInterval)
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div className="about_app">
|
return <div className="about_app">
|
||||||
@ -172,33 +154,13 @@ export default {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<LatencyIndicator
|
||||||
style={{
|
type="http"
|
||||||
display: "flex",
|
/>
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icons.MdHttp />
|
|
||||||
<antd.Tag
|
|
||||||
color={latencyToColor(connectionPing?.http, "http")}
|
|
||||||
>
|
|
||||||
{connectionPing?.http}ms
|
|
||||||
</antd.Tag>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<LatencyIndicator
|
||||||
style={{
|
type="ws"
|
||||||
display: "flex",
|
/>
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icons.MdSettingsEthernet />
|
|
||||||
<antd.Tag
|
|
||||||
color={latencyToColor(connectionPing?.ws, "ws")}
|
|
||||||
>
|
|
||||||
{connectionPing?.ws}ms
|
|
||||||
</antd.Tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,8 +62,6 @@ const SessionItem = (props) => {
|
|||||||
return UAParser(session.client)
|
return UAParser(session.client)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(session, ua)
|
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
"security_sessions_list_item_wrapper",
|
"security_sessions_list_item_wrapper",
|
||||||
|
@ -21,8 +21,6 @@ export default () => {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(response)
|
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
setSessions(response)
|
setSessions(response)
|
||||||
}
|
}
|
||||||
@ -72,7 +70,6 @@ export default () => {
|
|||||||
return `${total} Sessions`
|
return `${total} Sessions`
|
||||||
}}
|
}}
|
||||||
simple
|
simple
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,13 +72,16 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "low_performance_mode",
|
id: "low_performance_mode",
|
||||||
storaged: true,
|
|
||||||
group: "general",
|
group: "general",
|
||||||
component: "Switch",
|
component: "Switch",
|
||||||
icon: "MdSlowMotionVideo",
|
icon: "MdSlowMotionVideo",
|
||||||
title: "Low performance mode",
|
title: "Low performance mode",
|
||||||
description: "Enable low performance mode to reduce the memory usage and improve the performance in low-end devices. This will disable some animations and other decorative features.",
|
description: "Enable low performance mode to reduce the memory usage and improve the performance in low-end devices. This will disable some animations and other decorative features.",
|
||||||
emitEvent: "app.lowPerformanceMode",
|
emitEvent: "app.lowPerformanceMode",
|
||||||
|
props: {
|
||||||
|
disabled: true
|
||||||
|
},
|
||||||
|
storaged: true,
|
||||||
experimental: true,
|
experimental: true,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
@ -183,6 +186,23 @@ export default {
|
|||||||
storaged: true,
|
storaged: true,
|
||||||
mobile: false,
|
mobile: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "transcode_video_browser",
|
||||||
|
group: "posts",
|
||||||
|
component: "Switch",
|
||||||
|
icon: "MdVideoCameraFront",
|
||||||
|
title: "Transcode video in browser",
|
||||||
|
description: "Transcode videos from the application instead of on the servers. This feature may speed up the posting process depending on your computer. This will consume your computer resources.",
|
||||||
|
dependsOn: {
|
||||||
|
"low_performance_mode": false,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
experimental: true,
|
||||||
|
storaged: true,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "feed_max_fetch",
|
id: "feed_max_fetch",
|
||||||
title: "Fetch max items",
|
title: "Fetch max items",
|
||||||
|
@ -28,28 +28,28 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "fullName",
|
id: "public_name",
|
||||||
group: "account.basicInfo",
|
group: "account.basicInfo",
|
||||||
component: "Input",
|
component: "Input",
|
||||||
icon: "Edit3",
|
icon: "Edit3",
|
||||||
title: "Name",
|
title: "Name",
|
||||||
description: "Change your public name",
|
description: "Change your public name",
|
||||||
props: {
|
props: {
|
||||||
// set max length
|
maxLength: 120,
|
||||||
"maxLength": 120,
|
showCount: true,
|
||||||
"showCount": true,
|
allowClear: true,
|
||||||
"allowClear": true,
|
placeholder: "Enter your name. e.g. John Doe",
|
||||||
"placeholder": "Enter your name. e.g. John Doe",
|
|
||||||
},
|
},
|
||||||
defaultValue: (ctx) => {
|
defaultValue: (ctx) => {
|
||||||
return ctx.userData.fullName
|
return ctx.userData.public_name
|
||||||
},
|
},
|
||||||
onUpdate: async (value) => {
|
onUpdate: async (value) => {
|
||||||
const result = await UserModel.updateData({
|
const result = await UserModel.updateData({
|
||||||
fullName: value
|
public_name: value
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
app.message.success("Public name updated")
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -67,22 +67,22 @@ export default {
|
|||||||
storaged: false,
|
storaged: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "email",
|
id: "email",
|
||||||
"group": "account.basicInfo",
|
group: "account.basicInfo",
|
||||||
"component": "Input",
|
component: "Input",
|
||||||
"icon": "Mail",
|
icon: "Mail",
|
||||||
"title": "Email",
|
title: "Email",
|
||||||
"description": "Change your email address",
|
description: "Change your email address",
|
||||||
"props": {
|
props: {
|
||||||
"placeholder": "Enter your email address",
|
placeholder: "Enter your email address",
|
||||||
"allowClear": true,
|
allowClear: true,
|
||||||
"showCount": true,
|
showCount: true,
|
||||||
"maxLength": 320,
|
maxLength: 320,
|
||||||
},
|
},
|
||||||
"defaultValue": (ctx) => {
|
defaultValue: (ctx) => {
|
||||||
return ctx.userData.email
|
return ctx.userData.email
|
||||||
},
|
},
|
||||||
"onUpdate": async (value) => {
|
onUpdate: async (value) => {
|
||||||
const result = await UserModel.updateData({
|
const result = await UserModel.updateData({
|
||||||
email: value
|
email: value
|
||||||
})
|
})
|
||||||
@ -91,22 +91,22 @@ export default {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"debounced": true,
|
debounced: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "avatar",
|
id: "avatar",
|
||||||
"group": "account.profile",
|
group: "account.profile",
|
||||||
"icon": "Image",
|
icon: "Image",
|
||||||
"title": "Avatar",
|
title: "Avatar",
|
||||||
"description": "Change your avatar (Upload an image or use an URL)",
|
description: "Change your avatar (Upload an image or use an URL)",
|
||||||
"component": loadable(() => import("../components/urlInput")),
|
component: loadable(() => import("../components/urlInput")),
|
||||||
extraActions: [
|
extraActions: [
|
||||||
UploadButton
|
UploadButton
|
||||||
],
|
],
|
||||||
"defaultValue": (ctx) => {
|
defaultValue: (ctx) => {
|
||||||
return ctx.userData.avatar
|
return ctx.userData.avatar
|
||||||
},
|
},
|
||||||
"onUpdate": async (value) => {
|
onUpdate: async (value) => {
|
||||||
const result = await UserModel.updateData({
|
const result = await UserModel.updateData({
|
||||||
avatar: value
|
avatar: value
|
||||||
})
|
})
|
||||||
@ -118,19 +118,19 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cover",
|
id: "cover",
|
||||||
"group": "account.profile",
|
group: "account.profile",
|
||||||
"icon": "Image",
|
icon: "Image",
|
||||||
"title": "Cover",
|
title: "Cover",
|
||||||
"description": "Change your profile cover (Upload an image or use an URL)",
|
description: "Change your profile cover (Upload an image or use an URL)",
|
||||||
"component": loadable(() => import("../components/urlInput")),
|
component: loadable(() => import("../components/urlInput")),
|
||||||
extraActions: [
|
extraActions: [
|
||||||
UploadButton
|
UploadButton
|
||||||
],
|
],
|
||||||
"defaultValue": (ctx) => {
|
defaultValue: (ctx) => {
|
||||||
return ctx.userData.cover
|
return ctx.userData.cover
|
||||||
},
|
},
|
||||||
"onUpdate": async (value) => {
|
onUpdate: async (value) => {
|
||||||
const result = await UserModel.updateData({
|
const result = await UserModel.updateData({
|
||||||
cover: value
|
cover: value
|
||||||
})
|
})
|
||||||
@ -142,22 +142,22 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "description",
|
id: "description",
|
||||||
"group": "account.profile",
|
group: "account.profile",
|
||||||
"component": "TextArea",
|
component: "TextArea",
|
||||||
"icon": "Edit3",
|
icon: "Edit3",
|
||||||
"title": "Description",
|
title: "Description",
|
||||||
"description": "Change your description for your profile",
|
description: "Change your description for your profile",
|
||||||
"props": {
|
props: {
|
||||||
"placeholder": "Enter here a description for your profile",
|
placeholder: "Enter here a description for your profile",
|
||||||
"maxLength": 320,
|
maxLength: 320,
|
||||||
"showCount": true,
|
showCount: true,
|
||||||
"allowClear": true
|
allowClear: true
|
||||||
},
|
},
|
||||||
"defaultValue": (ctx) => {
|
defaultValue: (ctx) => {
|
||||||
return ctx.userData.description
|
return ctx.userData.description
|
||||||
},
|
},
|
||||||
"onUpdate": async (value) => {
|
onUpdate: async (value) => {
|
||||||
const result = await UserModel.updateData({
|
const result = await UserModel.updateData({
|
||||||
description: value
|
description: value
|
||||||
})
|
})
|
||||||
@ -166,8 +166,7 @@ export default {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"debounced": true,
|
debounced: true,
|
||||||
storaged: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "Links",
|
id: "Links",
|
||||||
@ -194,7 +193,6 @@ export default {
|
|||||||
return ctx.userData.links ?? []
|
return ctx.userData.links ?? []
|
||||||
},
|
},
|
||||||
debounced: true,
|
debounced: true,
|
||||||
storaged: false,
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -7,28 +7,31 @@ export default {
|
|||||||
group: "basic",
|
group: "basic",
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
"id": "change-password",
|
id: "change-password",
|
||||||
"group": "security.account",
|
group: "security.account",
|
||||||
"title": "Change Password",
|
title: "Change Password",
|
||||||
"description": "Change your password",
|
description: "Change your password",
|
||||||
"icon": "Lock",
|
icon: "Lock",
|
||||||
"component": loadable(() => import("../components/changePassword")),
|
component: loadable(() => import("../components/changePassword")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "two-factor-authentication",
|
id: "auth:mfa",
|
||||||
"group": "security.account",
|
group: "security.account",
|
||||||
"title": "Two-Factor Authentication",
|
title: "2-Factor Authentication",
|
||||||
"description": "Add an extra layer of security to your account",
|
description: "Use your email to validate logins to your account through a numerical code.",
|
||||||
"icon": "MdOutlineSecurity",
|
icon: "IoMdKeypad",
|
||||||
"component": "Switch",
|
component: "Switch",
|
||||||
|
defaultValue: (ctx) => {
|
||||||
|
return ctx.baseConfig["auth:mfa"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sessions",
|
id: "sessions",
|
||||||
"group": "security.account",
|
group: "security.account",
|
||||||
"title": "Sessions",
|
title: "Sessions",
|
||||||
"description": "Manage your active sessions",
|
description: "Manage your active sessions",
|
||||||
"icon": "Monitor",
|
icon: "Monitor",
|
||||||
"component": loadable(() => import("../components/sessions")),
|
component: loadable(() => import("../components/sessions")),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -228,6 +228,12 @@ class OwnTags extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.state.data) {
|
||||||
|
return <antd.Empty
|
||||||
|
description="You don't have any tags yet."
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="tap-share-own_tags">
|
return <div className="tap-share-own_tags">
|
||||||
{
|
{
|
||||||
this.state.data.length === 0 && <antd.Empty
|
this.state.data.length === 0 && <antd.Empty
|
||||||
|
@ -28,7 +28,6 @@
|
|||||||
"id": "Marketplace",
|
"id": "Marketplace",
|
||||||
"path": "/marketplace",
|
"path": "/marketplace",
|
||||||
"title": "Marketplace",
|
"title": "Marketplace",
|
||||||
"icon": "Box",
|
"icon": "Box"
|
||||||
"disabled": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
@ -158,6 +158,9 @@ class ComtyApp extends React.Component {
|
|||||||
"clearAllOverlays": function () {
|
"clearAllOverlays": function () {
|
||||||
window.app.DrawerController.closeAll()
|
window.app.DrawerController.closeAll()
|
||||||
},
|
},
|
||||||
|
"app.clearInternalStorage": function () {
|
||||||
|
app.clearInternalStorage()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
static publicMethods = {
|
static publicMethods = {
|
||||||
@ -248,8 +251,11 @@ class ComtyApp extends React.Component {
|
|||||||
/>)
|
/>)
|
||||||
},
|
},
|
||||||
|
|
||||||
openPostCreator: () => {
|
openPostCreator: (params) => {
|
||||||
app.layout.modal.open("post_creator", (props) => <PostCreator {...props} />, {
|
app.layout.modal.open("post_creator", (props) => <PostCreator
|
||||||
|
{...props}
|
||||||
|
{...params}
|
||||||
|
/>, {
|
||||||
framed: false
|
framed: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
import { Icons } from "components/Icons"
|
|
||||||
import * as antd from "antd"
|
|
||||||
import { getBase64 } from "utils"
|
|
||||||
|
|
||||||
export default class ImageUploader extends React.Component {
|
|
||||||
state = {
|
|
||||||
previewVisible: false,
|
|
||||||
previewImage: "",
|
|
||||||
previewTitle: "",
|
|
||||||
fileList: [],
|
|
||||||
urlList: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
api = window.app.cores.api.withEndpoints()
|
|
||||||
|
|
||||||
handleChange = ({ fileList }) => {
|
|
||||||
this.setState({ fileList })
|
|
||||||
|
|
||||||
if (typeof this.props.onChange === "function") {
|
|
||||||
this.props.onChange(fileList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancel = () => this.setState({ previewVisible: false })
|
|
||||||
|
|
||||||
handlePreview = async file => {
|
|
||||||
if (!file.url && !file.preview) {
|
|
||||||
file.preview = await getBase64(file.originFileObj)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
previewImage: file.url || file.preview,
|
|
||||||
previewVisible: true,
|
|
||||||
previewTitle: file.name || file.url.substring(file.url.lastIndexOf("/") + 1),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUploadRequest = async (req) => {
|
|
||||||
if (typeof this.props.onUpload === "function") {
|
|
||||||
this.props.onUpload(req)
|
|
||||||
} else {
|
|
||||||
const payloadData = new FormData()
|
|
||||||
payloadData.append(req.file.name, req.file)
|
|
||||||
|
|
||||||
const result = await this.api.post.upload(payloadData).catch(() => {
|
|
||||||
req.onError("Error uploading image")
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
req.onSuccess()
|
|
||||||
await this.setState({ urlList: [...this.state.urlList, ...result.urls] })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof this.props.onUploadDone === "function") {
|
|
||||||
await this.props.onUploadDone(this.state.urlList)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.urls
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const uploadButton = (<div>
|
|
||||||
<Icons.Plus />
|
|
||||||
<div style={{ marginTop: 8 }}>Upload</div>
|
|
||||||
</div>)
|
|
||||||
|
|
||||||
return <div>
|
|
||||||
<antd.Upload
|
|
||||||
listType="picture-card"
|
|
||||||
fileList={this.state.fileList}
|
|
||||||
onPreview={this.handlePreview}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
customRequest={this.handleUploadRequest}
|
|
||||||
>
|
|
||||||
{this.state.fileList.length >= 8 ? null : uploadButton}
|
|
||||||
</antd.Upload>
|
|
||||||
<antd.Modal
|
|
||||||
visible={this.state.previewVisible}
|
|
||||||
title={this.state.previewTitle}
|
|
||||||
footer={null}
|
|
||||||
onCancel={this.handleCancel}
|
|
||||||
>
|
|
||||||
<img style={{ width: "100%" }} src={this.state.previewImage} />
|
|
||||||
</antd.Modal>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
@ -42,7 +42,7 @@ export default class LiveChat extends React.Component {
|
|||||||
|
|
||||||
timelineRef = React.createRef()
|
timelineRef = React.createRef()
|
||||||
|
|
||||||
socket = app.cores.api.instance().wsInstances.chat
|
socket = app.cores.api.instance().sockets.chat
|
||||||
|
|
||||||
roomEvents = {
|
roomEvents = {
|
||||||
"room:recive:message": (message) => {
|
"room:recive:message": (message) => {
|
||||||
|
@ -50,21 +50,14 @@ export default React.forwardRef((props, ref) => {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
<div style={{ clear: "both" }} />
|
<lb style={{ clear: "both" }} />
|
||||||
|
|
||||||
<div
|
<lb
|
||||||
id="bottom"
|
id="bottom"
|
||||||
className="bottom"
|
className="bottom"
|
||||||
style={{ display: hasMore ? "block" : "none" }}
|
style={{ display: hasMore ? "block" : "none" }}
|
||||||
>
|
>
|
||||||
{loadingComponent && React.createElement(loadingComponent)}
|
{loadingComponent && React.createElement(loadingComponent)}
|
||||||
</div>
|
</lb>
|
||||||
|
|
||||||
{/* <div
|
|
||||||
className="no-result"
|
|
||||||
style={{ display: hasMore ? "none" : "block" }}
|
|
||||||
>
|
|
||||||
{noResultComponent ? React.createElement(noResultComponent) : "No more result"}
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
})
|
})
|
@ -200,7 +200,8 @@ export default class Login extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: to
|
phase: to,
|
||||||
|
mfa_required: null,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
|
||||||
|
import { createIconRender } from "components/Icons"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const latencyToColor = (latency, type) => {
|
||||||
|
switch (type) {
|
||||||
|
case "http": {
|
||||||
|
if (latency < 200) {
|
||||||
|
return "green"
|
||||||
|
}
|
||||||
|
if (latency < 500) {
|
||||||
|
return "orange"
|
||||||
|
}
|
||||||
|
return "red"
|
||||||
|
}
|
||||||
|
case "ws": {
|
||||||
|
if (latency < 80) {
|
||||||
|
return "green"
|
||||||
|
}
|
||||||
|
if (latency < 120) {
|
||||||
|
return "orange"
|
||||||
|
}
|
||||||
|
return "red"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TypesDecorator = {
|
||||||
|
http: {
|
||||||
|
label: "HTTP",
|
||||||
|
icon: "MdHttp",
|
||||||
|
},
|
||||||
|
ws: {
|
||||||
|
label: "WS",
|
||||||
|
icon: "MdSettingsEthernet",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LatencyIndicator = (props) => {
|
||||||
|
const { type } = props
|
||||||
|
const [latencyMs, setLatencyMs] = React.useState("0")
|
||||||
|
|
||||||
|
const decorator = TypesDecorator[type]
|
||||||
|
|
||||||
|
if (!decorator) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateLatency() {
|
||||||
|
if (typeof props.calculateLatency === "function") {
|
||||||
|
return setLatencyMs(props.calculateLatency())
|
||||||
|
}
|
||||||
|
|
||||||
|
app.cores.api.measurePing({
|
||||||
|
select: [type]
|
||||||
|
}).then((result) => {
|
||||||
|
setLatencyMs(result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
calculateLatency()
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
calculateLatency()
|
||||||
|
}, props.interval ?? 3000)
|
||||||
|
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className="latencyIndicator"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
decorator.icon && createIconRender(decorator.icon)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!decorator.icon && (decorator.label ?? "Latency")
|
||||||
|
}
|
||||||
|
<antd.Tag
|
||||||
|
color={latencyToColor(latencyMs, type)}
|
||||||
|
>
|
||||||
|
{latencyMs}ms
|
||||||
|
</antd.Tag>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LatencyIndicator
|
@ -0,0 +1,13 @@
|
|||||||
|
.latencyIndicator {
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
gap: 7px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ import { Icons } from "components/Icons"
|
|||||||
|
|
||||||
import SaveButton from "./saveButton"
|
import SaveButton from "./saveButton"
|
||||||
import LikeButton from "./likeButton"
|
import LikeButton from "./likeButton"
|
||||||
import CommentsButton from "./commentsButton"
|
import RepliesButton from "./replyButton"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ const MoreActionsItems = [
|
|||||||
{
|
{
|
||||||
key: "onClickRepost",
|
key: "onClickRepost",
|
||||||
label: <>
|
label: <>
|
||||||
<Icons.Repeat />
|
<Icons.MdCallSplit />
|
||||||
<span>Repost</span>
|
<span>Repost</span>
|
||||||
</>,
|
</>,
|
||||||
},
|
},
|
||||||
@ -61,7 +61,7 @@ export default (props) => {
|
|||||||
const {
|
const {
|
||||||
onClickLike,
|
onClickLike,
|
||||||
onClickSave,
|
onClickSave,
|
||||||
onClickComments,
|
onClickReply,
|
||||||
} = props.actions ?? {}
|
} = props.actions ?? {}
|
||||||
|
|
||||||
const genItems = () => {
|
const genItems = () => {
|
||||||
@ -95,10 +95,10 @@ export default (props) => {
|
|||||||
onClick={onClickSave}
|
onClick={onClickSave}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="action" id="comments">
|
<div className="action" id="replies">
|
||||||
<CommentsButton
|
<RepliesButton
|
||||||
count={props.commentsCount}
|
count={props.repliesCount}
|
||||||
onClick={onClickComments}
|
onClick={onClickReply}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="action" id="more">
|
<div className="action" id="more">
|
||||||
|
@ -6,16 +6,16 @@ import "./index.less"
|
|||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
return <div
|
return <div
|
||||||
className="comments_button"
|
className="reply_button"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="ghost"
|
type="ghost"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
icon={<Icons.MessageCircle />}
|
icon={<Icons.Repeat />}
|
||||||
/>
|
/>
|
||||||
{
|
{
|
||||||
props.count > 0 && <span className="comments_count">{props.count}</span>
|
props.count > 0 && <span className="replies_count">{props.count}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
.comments_button {
|
.reply_button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.comments_count {
|
.replies_count {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,13 +1,29 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
import { Tag } from "antd"
|
import { Tag, Skeleton } from "antd"
|
||||||
|
|
||||||
import { Image } from "components"
|
import { Image } from "components"
|
||||||
import { Icons } from "components/Icons"
|
import { Icons } from "components/Icons"
|
||||||
|
import PostLink from "components/PostLink"
|
||||||
|
|
||||||
|
import PostService from "models/post"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
export default (props) => {
|
const PostReplieView = (props) => {
|
||||||
|
const { data } = props
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
@{data.user.username}
|
||||||
|
{data.message}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PostCardHeader = (props) => {
|
||||||
const [timeAgo, setTimeAgo] = React.useState(0)
|
const [timeAgo, setTimeAgo] = React.useState(0)
|
||||||
|
|
||||||
const goToProfile = () => {
|
const goToProfile = () => {
|
||||||
@ -17,7 +33,12 @@ export default (props) => {
|
|||||||
const updateTimeAgo = () => {
|
const updateTimeAgo = () => {
|
||||||
let createdAt = props.postData.timestamp ?? props.postData.created_at ?? ""
|
let createdAt = props.postData.timestamp ?? props.postData.created_at ?? ""
|
||||||
|
|
||||||
const timeAgo = DateTime.fromISO(createdAt, { locale: app.cores.settings.get("language") }).toRelative()
|
const timeAgo = DateTime.fromISO(
|
||||||
|
createdAt,
|
||||||
|
{
|
||||||
|
locale: app.cores.settings.get("language")
|
||||||
|
}
|
||||||
|
).toRelative()
|
||||||
|
|
||||||
setTimeAgo(timeAgo)
|
setTimeAgo(timeAgo)
|
||||||
}
|
}
|
||||||
@ -34,22 +55,41 @@ export default (props) => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div className="post_header" onDoubleClick={props.onDoubleClick}>
|
return <div className="post-header" onDoubleClick={props.onDoubleClick}>
|
||||||
<div className="user">
|
{
|
||||||
<div className="avatar">
|
!props.disableReplyTag && props.postData.reply_to && <div
|
||||||
|
className="post-header-replied_to"
|
||||||
|
>
|
||||||
|
<Icons.Repeat />
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Replied to
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<PostReplieView
|
||||||
|
data={props.postData.reply_to_data}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className="post-header-user">
|
||||||
|
<div className="post-header-user-avatar">
|
||||||
<Image
|
<Image
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
src={props.postData.user?.avatar}
|
src={props.postData.user?.avatar}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="info">
|
|
||||||
|
<div className="post-header-user-info">
|
||||||
<h1 onClick={goToProfile}>
|
<h1 onClick={goToProfile}>
|
||||||
{
|
{
|
||||||
props.postData.user?.fullName ?? `${props.postData.user?.username}`
|
props.postData.user?.public_name ?? `${props.postData.user?.username}`
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
props.postData.user?.verified && <Icons.verifiedBadge />
|
props.postData.user?.verified && <Icons.verifiedBadge />
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
props.postData.flags?.includes("nsfw") && <Tag
|
props.postData.flags?.includes("nsfw") && <Tag
|
||||||
color="volcano"
|
color="volcano"
|
||||||
@ -59,10 +99,12 @@ export default (props) => {
|
|||||||
}
|
}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<span className="timeago">
|
<span className="post-header-user-info-timeago">
|
||||||
{timeAgo}
|
{timeAgo}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default PostCardHeader
|
@ -1,9 +1,26 @@
|
|||||||
.post_header {
|
.post-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.user {
|
gap: 10px;
|
||||||
|
|
||||||
|
.post-header-replied_to {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 7px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--text-color);
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-header-user {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -17,7 +34,7 @@
|
|||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.post-header-user-avatar {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
||||||
@ -30,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.post-header-user-info {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
@ -51,7 +68,7 @@
|
|||||||
color: var(--background-color-contrast);
|
color: var(--background-color-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeago {
|
.post-header-user-info-timeago {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
|
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import Plyr from "plyr-react"
|
import Plyr from "plyr-react"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
import { CommentsCard } from "components"
|
|
||||||
import { Icons } from "components/Icons"
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
import { processString } from "utils"
|
import { processString } from "utils"
|
||||||
|
|
||||||
import PostHeader from "./components/header"
|
import PostHeader from "./components/header"
|
||||||
@ -13,6 +10,7 @@ import PostActions from "./components/actions"
|
|||||||
import PostAttachments from "./components/attachments"
|
import PostAttachments from "./components/attachments"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
import { Divider } from "antd"
|
||||||
|
|
||||||
const messageRegexs = [
|
const messageRegexs = [
|
||||||
{
|
{
|
||||||
@ -43,11 +41,14 @@ const messageRegexs = [
|
|||||||
|
|
||||||
export default class PostCard extends React.PureComponent {
|
export default class PostCard extends React.PureComponent {
|
||||||
state = {
|
state = {
|
||||||
|
data: this.props.data,
|
||||||
|
|
||||||
countLikes: this.props.data.countLikes ?? 0,
|
countLikes: this.props.data.countLikes ?? 0,
|
||||||
countComments: this.props.data.countComments ?? 0,
|
countReplies: this.props.data.countComments ?? 0,
|
||||||
|
|
||||||
hasLiked: this.props.data.isLiked ?? false,
|
hasLiked: this.props.data.isLiked ?? false,
|
||||||
hasSaved: this.props.data.isSaved ?? false,
|
hasSaved: this.props.data.isSaved ?? false,
|
||||||
|
hasReplies: this.props.data.hasReplies ?? false,
|
||||||
|
|
||||||
open: this.props.defaultOpened ?? false,
|
open: this.props.defaultOpened ?? false,
|
||||||
|
|
||||||
@ -55,13 +56,28 @@ export default class PostCard extends React.PureComponent {
|
|||||||
nsfwAccepted: false,
|
nsfwAccepted: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDataUpdate = (data) => {
|
||||||
|
this.setState({
|
||||||
|
data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
onClickDelete = async () => {
|
onClickDelete = async () => {
|
||||||
if (typeof this.props.events.onClickDelete !== "function") {
|
if (typeof this.props.events.onClickDelete !== "function") {
|
||||||
console.warn("onClickDelete event is not a function")
|
console.warn("onClickDelete event is not a function")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.props.events.onClickDelete(this.props.data)
|
return await this.props.events.onClickDelete(this.state.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickLike = async () => {
|
onClickLike = async () => {
|
||||||
@ -70,7 +86,16 @@ export default class PostCard extends React.PureComponent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.props.events.onClickLike(this.props.data)
|
const actionResult = await this.props.events.onClickLike(this.state.data)
|
||||||
|
|
||||||
|
if (actionResult) {
|
||||||
|
this.setState({
|
||||||
|
hasLiked: actionResult.liked,
|
||||||
|
countLikes: actionResult.count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionResult
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickSave = async () => {
|
onClickSave = async () => {
|
||||||
@ -79,7 +104,15 @@ export default class PostCard extends React.PureComponent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.props.events.onClickSave(this.props.data)
|
const actionResult = await this.props.events.onClickSave(this.state.data)
|
||||||
|
|
||||||
|
if (actionResult) {
|
||||||
|
this.setState({
|
||||||
|
hasSaved: actionResult.saved,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionResult
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickEdit = async () => {
|
onClickEdit = async () => {
|
||||||
@ -88,57 +121,26 @@ export default class PostCard extends React.PureComponent {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.props.events.onClickEdit(this.props.data)
|
return await this.props.events.onClickEdit(this.state.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
onDoubleClick = async () => {
|
onClickReply = async () => {
|
||||||
this.handleOpen()
|
if (typeof this.props.events.onClickReply !== "function") {
|
||||||
}
|
console.warn("onClickReply event is not a function")
|
||||||
|
return
|
||||||
onClickComments = async () => {
|
|
||||||
this.handleOpen()
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOpen = (to) => {
|
|
||||||
if (typeof to === "undefined") {
|
|
||||||
to = !this.state.open
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof this.props.events?.ontoggleOpen === "function") {
|
return await this.props.events.onClickReply(this.state.data)
|
||||||
this.props.events?.ontoggleOpen(to, this.props.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
open: to,
|
|
||||||
})
|
|
||||||
|
|
||||||
//app.controls.openPostViewer(this.props.data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onLikesUpdate = (data) => {
|
componentDidUpdate = (prevProps) => {
|
||||||
console.log("onLikesUpdate", data)
|
if (prevProps.data !== this.props.data) {
|
||||||
|
|
||||||
if (data.to) {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
countLikes: this.state.countLikes + 1,
|
data: this.props.data,
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
countLikes: this.state.countLikes - 1,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount = async () => {
|
|
||||||
// first listen to post changes
|
|
||||||
app.cores.api.listenEvent(`post.${this.props.data._id}.likes.update`, this.onLikesUpdate)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
|
||||||
// remove the listener
|
|
||||||
app.cores.api.unlistenEvent(`post.${this.props.data._id}.likes.update`, this.onLikesUpdate)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch = (error, info) => {
|
componentDidCatch = (error, info) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|
||||||
@ -153,12 +155,28 @@ export default class PostCard extends React.PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <article
|
return <motion.div
|
||||||
|
initial={{ y: -100, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1, }}
|
||||||
|
exit={{ scale: 0, opacity: 0 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.1,
|
||||||
|
}}
|
||||||
|
layout
|
||||||
key={this.props.index}
|
key={this.props.index}
|
||||||
id={this.props.data._id}
|
id={this.state.data._id}
|
||||||
|
post_id={this.state.data._id}
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
user-id={this.props.data.user_id}
|
user-id={this.state.data.user_id}
|
||||||
context-menu={"postCard-context"}
|
context-menu={"postCard-context"}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
"post_card",
|
"post_card",
|
||||||
@ -168,8 +186,9 @@ export default class PostCard extends React.PureComponent {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<PostHeader
|
<PostHeader
|
||||||
postData={this.props.data}
|
postData={this.state.data}
|
||||||
onDoubleClick={this.onDoubleClick}
|
onDoubleClick={this.onDoubleClick}
|
||||||
|
disableReplyTag={this.props.disableReplyTag}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -180,37 +199,43 @@ export default class PostCard extends React.PureComponent {
|
|||||||
>
|
>
|
||||||
<div className="message">
|
<div className="message">
|
||||||
{
|
{
|
||||||
processString(messageRegexs)(this.props.data.message ?? "")
|
processString(messageRegexs)(this.state.data.message ?? "")
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
!this.props.disableAttachments && this.props.data.attachments && this.props.data.attachments.length > 0 && <PostAttachments
|
!this.props.disableAttachments && this.state.data.attachments && this.state.data.attachments.length > 0 && <PostAttachments
|
||||||
attachments={this.props.data.attachments}
|
attachments={this.state.data.attachments}
|
||||||
flags={this.props.data.flags}
|
flags={this.state.data.flags}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PostActions
|
<PostActions
|
||||||
user_id={this.props.data.user_id}
|
user_id={this.state.data.user_id}
|
||||||
|
|
||||||
likesCount={this.state.countLikes}
|
likesCount={this.state.countLikes}
|
||||||
commentsCount={this.state.countComments}
|
repliesCount={this.state.countReplies}
|
||||||
|
|
||||||
defaultLiked={this.state.hasLiked}
|
defaultLiked={this.state.hasLiked}
|
||||||
defaultSaved={this.state.hasSaved}
|
defaultSaved={this.state.hasSaved}
|
||||||
|
|
||||||
actions={{
|
actions={{
|
||||||
onClickLike: this.onClickLike,
|
onClickLike: this.onClickLike,
|
||||||
onClickEdit: this.onClickEdit,
|
onClickEdit: this.onClickEdit,
|
||||||
onClickDelete: this.onClickDelete,
|
onClickDelete: this.onClickDelete,
|
||||||
onClickSave: this.onClickSave,
|
onClickSave: this.onClickSave,
|
||||||
onClickComments: this.onClickComments,
|
onClickReply: this.onClickReply,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CommentsCard
|
{
|
||||||
post_id={this.props.data._id}
|
!this.props.disableHasReplies && this.state.hasReplies && <>
|
||||||
visible={this.state.open}
|
<Divider />
|
||||||
/>
|
<h1>View replies</h1>
|
||||||
</article>
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
</motion.div>
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -13,7 +13,7 @@
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
padding: 17px 17px 0px 17px;
|
padding: 17px 17px 10px 17px;
|
||||||
|
|
||||||
background-color: var(--background-color-accent);
|
background-color: var(--background-color-accent);
|
||||||
|
|
||||||
@ -22,8 +22,6 @@
|
|||||||
|
|
||||||
color: rgba(var(--background-color-contrast));
|
color: rgba(var(--background-color-contrast));
|
||||||
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
|
@ -1,24 +1,21 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import { DateTime } from "luxon"
|
|
||||||
import humanSize from "@tsmx/human-readable"
|
import humanSize from "@tsmx/human-readable"
|
||||||
|
import PostLink from "components/PostLink"
|
||||||
import { Icons } from "components/Icons"
|
import { Icons } from "components/Icons"
|
||||||
import clipboardEventFileToFile from "utils/clipboardEventFileToFile"
|
|
||||||
|
|
||||||
|
import clipboardEventFileToFile from "utils/clipboardEventFileToFile"
|
||||||
import PostModel from "models/post"
|
import PostModel from "models/post"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const DEFAULT_POST_POLICY = {
|
const DEFAULT_POST_POLICY = {
|
||||||
maxMessageLength: 512,
|
maxMessageLength: 512,
|
||||||
acceptedMimeTypes: ["image/gif", "image/png", "image/jpeg", "image/bmp"],
|
acceptedMimeTypes: ["image/gif", "image/png", "image/jpeg", "image/bmp", "video/*"],
|
||||||
maximumFileSize: 10 * 1024 * 1024,
|
|
||||||
maximunFilesPerRequest: 10
|
maximunFilesPerRequest: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fix close window when post created
|
|
||||||
|
|
||||||
export default class PostCreator extends React.Component {
|
export default class PostCreator extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
@ -92,15 +89,30 @@ export default class PostCreator extends React.Component {
|
|||||||
const payload = {
|
const payload = {
|
||||||
message: postMessage,
|
message: postMessage,
|
||||||
attachments: postAttachments,
|
attachments: postAttachments,
|
||||||
timestamp: DateTime.local().toISO(),
|
//timestamp: DateTime.local().toISO(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await PostModel.create(payload).catch(error => {
|
let response = null
|
||||||
console.error(error)
|
|
||||||
antd.message.error(error)
|
|
||||||
|
|
||||||
return false
|
if (this.props.reply_to) {
|
||||||
})
|
payload.reply_to = this.props.reply_to
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.edit_post) {
|
||||||
|
response = await PostModel.update(this.props.edit_post, payload).catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
antd.message.error(error)
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
response = await PostModel.create(payload).catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
antd.message.error(error)
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false
|
loading: false
|
||||||
@ -116,6 +128,10 @@ export default class PostCreator extends React.Component {
|
|||||||
if (typeof this.props.close === "function") {
|
if (typeof this.props.close === "function") {
|
||||||
this.props.close()
|
this.props.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.reply_to) {
|
||||||
|
app.navigation.goToPost(this.props.reply_to)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,8 +198,6 @@ export default class PostCreator extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(change)
|
|
||||||
|
|
||||||
switch (change.file.status) {
|
switch (change.file.status) {
|
||||||
case "uploading": {
|
case "uploading": {
|
||||||
this.toggleUploaderVisibility(false)
|
this.toggleUploaderVisibility(false)
|
||||||
@ -424,9 +438,37 @@ export default class PostCreator extends React.Component {
|
|||||||
dialog.click()
|
dialog.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount = async () => {
|
||||||
|
if (this.props.edit_post) {
|
||||||
|
await this.setState({
|
||||||
|
loading: true,
|
||||||
|
postId: this.props.edit_post,
|
||||||
|
})
|
||||||
|
|
||||||
|
const post = await PostModel.getPost({ post_id: this.props.edit_post })
|
||||||
|
|
||||||
|
await this.setState({
|
||||||
|
loading: false,
|
||||||
|
postMessage: post.message,
|
||||||
|
postAttachments: post.attachments.map((attachment) => {
|
||||||
|
return {
|
||||||
|
...attachment,
|
||||||
|
uid: attachment.id,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
fileList: post.attachments.map((attachment) => {
|
||||||
|
return {
|
||||||
|
...attachment,
|
||||||
|
uid: attachment.id,
|
||||||
|
id: attachment.id,
|
||||||
|
thumbUrl: attachment.url,
|
||||||
|
status: "done",
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
// fetch the posting policy
|
// fetch the posting policy
|
||||||
this.fetchUploadPolicy()
|
//this.fetchUploadPolicy()
|
||||||
|
|
||||||
// add a listener to the window
|
// add a listener to the window
|
||||||
document.addEventListener("paste", this.handlePaste)
|
document.addEventListener("paste", this.handlePaste)
|
||||||
@ -448,6 +490,10 @@ export default class PostCreator extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { postMessage, fileList, loading, uploaderVisible, postingPolicy } = this.state
|
const { postMessage, fileList, loading, uploaderVisible, postingPolicy } = this.state
|
||||||
|
|
||||||
|
const editMode = !!this.props.edit_post
|
||||||
|
|
||||||
|
const showHeader = !!this.props.edit_post || this.props.reply_to
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
className={"postCreator"}
|
className={"postCreator"}
|
||||||
ref={this.creatorRef}
|
ref={this.creatorRef}
|
||||||
@ -455,6 +501,37 @@ export default class PostCreator extends React.Component {
|
|||||||
onDragLeave={this.handleDrag}
|
onDragLeave={this.handleDrag}
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
|
showHeader && <div className="postCreator-header">
|
||||||
|
{
|
||||||
|
this.props.edit_post && <div className="postCreator-header-indicator">
|
||||||
|
<p>
|
||||||
|
<Icons.MdEdit />
|
||||||
|
Editing post
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
this.props.reply_to && <div className="postCreator-header-indicator">
|
||||||
|
<p>
|
||||||
|
<Icons.MdReply />
|
||||||
|
Replaying to
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<PostLink
|
||||||
|
post_id={this.props.reply_to}
|
||||||
|
onClick={() => {
|
||||||
|
this.props.close()
|
||||||
|
app.navigation.goToPost(this.props.reply_to)
|
||||||
|
}}
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div className="textInput">
|
<div className="textInput">
|
||||||
<div className="avatar">
|
<div className="avatar">
|
||||||
<img src={app.userData?.avatar} />
|
<img src={app.userData?.avatar} />
|
||||||
@ -475,7 +552,7 @@ export default class PostCreator extends React.Component {
|
|||||||
type="primary"
|
type="primary"
|
||||||
disabled={loading || !this.canSubmit()}
|
disabled={loading || !this.canSubmit()}
|
||||||
onClick={this.submit}
|
onClick={this.submit}
|
||||||
icon={loading ? <Icons.LoadingOutlined spin /> : <Icons.Send />}
|
icon={loading ? <Icons.LoadingOutlined spin /> : (editMode ? <Icons.MdEdit /> : <Icons.Send />)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,29 @@
|
|||||||
background-color: var(--background-color-accent);
|
background-color: var(--background-color-accent);
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|
||||||
//transition: all 250ms ease-in-out;
|
gap: 10px;
|
||||||
|
|
||||||
|
.postCreator-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.postCreator-header-indicator {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
gap: 7px;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -58,8 +80,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-x: scroll;
|
overflow-x: scroll;
|
||||||
|
|
||||||
@ -125,6 +145,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@ -146,6 +168,8 @@
|
|||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|
||||||
border-radius: @file_preview_borderRadius;
|
border-radius: @file_preview_borderRadius;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
packages/app/src/components/PostLink/index.jsx
Normal file
28
packages/app/src/components/PostLink/index.jsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { Tag } from "antd"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const PostLink = (props) => {
|
||||||
|
if (!props.post_id) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Tag
|
||||||
|
className="post-link"
|
||||||
|
color="geekblue"
|
||||||
|
onClick={() => {
|
||||||
|
if (props.onClick) {
|
||||||
|
return props.onClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
app.navigation.goToPost(props.post_id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
#{props.post_id}
|
||||||
|
</span>
|
||||||
|
</Tag>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostLink
|
24
packages/app/src/components/PostLink/index.less
Normal file
24
packages/app/src/components/PostLink/index.less
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
.post-link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 800;
|
||||||
|
|
||||||
|
font-family: "DM Mono", monospace;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
span {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import { Icons } from "components/Icons"
|
import { Icons } from "components/Icons"
|
||||||
|
import { AnimatePresence } from "framer-motion"
|
||||||
|
|
||||||
import PostCard from "components/PostCard"
|
import PostCard from "components/PostCard"
|
||||||
import PlaylistTimelineEntry from "components/Music/PlaylistTimelineEntry"
|
import PlaylistTimelineEntry from "components/Music/PlaylistTimelineEntry"
|
||||||
@ -41,21 +42,21 @@ const Entry = React.memo((props) => {
|
|||||||
return React.createElement(typeToComponent[data.type ?? "post"] ?? PostCard, {
|
return React.createElement(typeToComponent[data.type ?? "post"] ?? PostCard, {
|
||||||
key: data._id,
|
key: data._id,
|
||||||
data: data,
|
data: data,
|
||||||
//disableAttachments: true,
|
disableReplyTag: props.disableReplyTag,
|
||||||
events: {
|
events: {
|
||||||
onClickLike: props.onLikePost,
|
onClickLike: props.onLikePost,
|
||||||
onClickSave: props.onSavePost,
|
onClickSave: props.onSavePost,
|
||||||
onClickDelete: props.onDeletePost,
|
onClickDelete: props.onDeletePost,
|
||||||
onClickEdit: props.onEditPost,
|
onClickEdit: props.onEditPost,
|
||||||
|
onClickReply: props.onReplyPost,
|
||||||
|
onDoubleClick: props.onDoubleClick,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const PostList = (props) => {
|
const PostList = React.forwardRef((props, ref) => {
|
||||||
const parentRef = React.useRef()
|
|
||||||
|
|
||||||
return <LoadMore
|
return <LoadMore
|
||||||
ref={parentRef}
|
ref={ref}
|
||||||
className="post-list"
|
className="post-list"
|
||||||
loadingComponent={LoadingComponent}
|
loadingComponent={LoadingComponent}
|
||||||
noResultComponent={NoResultComponent}
|
noResultComponent={NoResultComponent}
|
||||||
@ -77,40 +78,20 @@ const PostList = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
<AnimatePresence>
|
||||||
props.list.map((data) => {
|
|
||||||
return <Entry
|
|
||||||
key={data._id}
|
|
||||||
data={data}
|
|
||||||
onLikePost={props.onLikePost}
|
|
||||||
onSavePost={props.onSavePost}
|
|
||||||
onDeletePost={props.onDeletePost}
|
|
||||||
onEditPost={props.onEditPost}
|
|
||||||
/>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* <For
|
|
||||||
each={props.list}
|
|
||||||
style={{
|
|
||||||
height: `100%`,
|
|
||||||
width: `100%`,
|
|
||||||
}}
|
|
||||||
as="div"
|
|
||||||
>
|
|
||||||
{
|
{
|
||||||
(data) => <Entry
|
props.list.map((data) => {
|
||||||
key={data._id}
|
return <Entry
|
||||||
data={data}
|
key={data._id}
|
||||||
onLikePost={props.onLikePost}
|
data={data}
|
||||||
onSavePost={props.onSavePost}
|
{...props}
|
||||||
onDeletePost={props.onDeletePost}
|
/>
|
||||||
onEditPost={props.onEditPost}
|
})
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
</For> */}
|
</AnimatePresence>
|
||||||
</LoadMore>
|
</LoadMore>
|
||||||
}
|
|
||||||
|
})
|
||||||
|
|
||||||
export class PostsListsComponent extends React.Component {
|
export class PostsListsComponent extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
@ -238,12 +219,15 @@ export class PostsListsComponent extends React.Component {
|
|||||||
addPost: this.addPost,
|
addPost: this.addPost,
|
||||||
removePost: this.removePost,
|
removePost: this.removePost,
|
||||||
addRandomPost: () => {
|
addRandomPost: () => {
|
||||||
|
const randomId = Math.random().toString(36).substring(7)
|
||||||
|
|
||||||
this.addPost({
|
this.addPost({
|
||||||
_id: Math.random().toString(36).substring(7),
|
_id: randomId,
|
||||||
message: `Random post ${Math.random().toString(36).substring(7)}`,
|
message: `Random post ${randomId}`,
|
||||||
user: {
|
user: {
|
||||||
_id: Math.random().toString(36).substring(7),
|
_id: randomId,
|
||||||
username: "random user",
|
username: "random user",
|
||||||
|
avatar: `https://api.dicebear.com/7.x/thumbs/svg?seed=${randomId}`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -336,7 +320,7 @@ export class PostsListsComponent extends React.Component {
|
|||||||
console.error(`The event "${event}" is not defined in the timelineWsEvents object`)
|
console.error(`The event "${event}" is not defined in the timelineWsEvents object`)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.cores.api.listenEvent(event, this.timelineWsEvents[event])
|
app.cores.api.listenEvent(event, this.timelineWsEvents[event], "posts")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -358,7 +342,7 @@ export class PostsListsComponent extends React.Component {
|
|||||||
console.error(`The event "${event}" is not defined in the timelineWsEvents object`)
|
console.error(`The event "${event}" is not defined in the timelineWsEvents object`)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.cores.api.unlistenEvent(event, this.timelineWsEvents[event])
|
app.cores.api.unlistenEvent(event, this.timelineWsEvents[event], "posts")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -370,7 +354,7 @@ export class PostsListsComponent extends React.Component {
|
|||||||
window._hacks = null
|
window._hacks = null
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate = async (prevProps) => {
|
componentDidUpdate = async (prevProps, prevState) => {
|
||||||
if (prevProps.list !== this.props.list) {
|
if (prevProps.list !== this.props.list) {
|
||||||
this.setState({
|
this.setState({
|
||||||
list: this.props.list,
|
list: this.props.list,
|
||||||
@ -398,6 +382,22 @@ export class PostsListsComponent extends React.Component {
|
|||||||
return result
|
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) => {
|
onDeletePost = async (data) => {
|
||||||
antd.Modal.confirm({
|
antd.Modal.confirm({
|
||||||
title: "Are you sure you want to delete this post?",
|
title: "Are you sure you want to delete this post?",
|
||||||
@ -444,13 +444,16 @@ export class PostsListsComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PostListProps = {
|
const PostListProps = {
|
||||||
listRef: this.listRef,
|
|
||||||
list: this.state.list,
|
list: this.state.list,
|
||||||
|
|
||||||
|
disableReplyTag: this.props.disableReplyTag,
|
||||||
|
|
||||||
onLikePost: this.onLikePost,
|
onLikePost: this.onLikePost,
|
||||||
onSavePost: this.onSavePost,
|
onSavePost: this.onSavePost,
|
||||||
onDeletePost: this.onDeletePost,
|
onDeletePost: this.onDeletePost,
|
||||||
onEditPost: this.onEditPost,
|
onEditPost: this.onEditPost,
|
||||||
|
onReplyPost: this.onReplyPost,
|
||||||
|
onDoubleClick: this.onDoubleClickPost,
|
||||||
|
|
||||||
onLoadMore: this.onLoadMore,
|
onLoadMore: this.onLoadMore,
|
||||||
hasMore: this.state.hasMore,
|
hasMore: this.state.hasMore,
|
||||||
@ -463,12 +466,14 @@ export class PostsListsComponent extends React.Component {
|
|||||||
|
|
||||||
if (app.isMobile) {
|
if (app.isMobile) {
|
||||||
return <PostList
|
return <PostList
|
||||||
|
ref={this.listRef}
|
||||||
{...PostListProps}
|
{...PostListProps}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="post-list_wrapper">
|
return <div className="post-list_wrapper">
|
||||||
<PostList
|
<PostList
|
||||||
|
ref={this.listRef}
|
||||||
{...PostListProps}
|
{...PostListProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,7 +59,7 @@ html {
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
// WARN: Only use if is a performance issue (If is using virtualized list)
|
// WARN: Only use if is a performance issue (If is using virtualized list)
|
||||||
will-change: transform;
|
//will-change: transform;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-y: overlay;
|
overflow-y: overlay;
|
||||||
@ -77,10 +77,14 @@ html {
|
|||||||
//margin: auto;
|
//margin: auto;
|
||||||
z-index: 150;
|
z-index: 150;
|
||||||
|
|
||||||
|
background-color: var(--background-color-accent);
|
||||||
|
|
||||||
.post_card {
|
.post_card {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border-bottom: 2px solid var(--border-color);
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
@ -95,33 +99,11 @@ html {
|
|||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
border-bottom-right-radius: 8px;
|
border-bottom-right-radius: 8px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.playlistTimelineEntry {
|
&:last-of-type {
|
||||||
border-radius: 0;
|
border-bottom: none;
|
||||||
border-bottom: 2px solid var(--border-color);
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-top-left-radius: 0px;
|
|
||||||
border-top-right-radius: 0px;
|
|
||||||
|
|
||||||
border-bottom-left-radius: 8px;
|
|
||||||
border-bottom-right-radius: 8px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.postCard {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
// FIXME: This is a walkaround for a bug when a post contains multiple attachments cause a overflow
|
|
||||||
max-width: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playlistTimelineEntry {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
max-width: unset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.resume_btn_wrapper {
|
.resume_btn_wrapper {
|
||||||
|
@ -52,7 +52,7 @@ export default class SyncRoomCard extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkLatency = () => {
|
checkLatency = () => {
|
||||||
const instance = app.cores.api.instance().wsInstances.music
|
const instance = app.cores.api.instance().sockets.music
|
||||||
|
|
||||||
if (instance) {
|
if (instance) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -67,7 +67,7 @@ export default class SyncRoomCard extends React.Component {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// chat instance
|
// chat instance
|
||||||
const chatInstance = app.cores.api.instance().wsInstances.chat
|
const chatInstance = app.cores.api.instance().sockets.chat
|
||||||
|
|
||||||
if (chatInstance) {
|
if (chatInstance) {
|
||||||
Object.keys(this.chatEvents).forEach((event) => {
|
Object.keys(this.chatEvents).forEach((event) => {
|
||||||
@ -92,7 +92,7 @@ export default class SyncRoomCard extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// chat instance
|
// chat instance
|
||||||
const chatInstance = app.cores.api.instance().wsInstances.chat
|
const chatInstance = app.cores.api.instance().sockets.chat
|
||||||
|
|
||||||
if (chatInstance) {
|
if (chatInstance) {
|
||||||
Object.keys(this.chatEvents).forEach((event) => {
|
Object.keys(this.chatEvents).forEach((event) => {
|
||||||
@ -231,7 +231,7 @@ export default class SyncRoomCard extends React.Component {
|
|||||||
<div className="latency_display">
|
<div className="latency_display">
|
||||||
<span>
|
<span>
|
||||||
{
|
{
|
||||||
app.cores.api.instance().wsInstances.music.latency ?? "..."
|
app.cores.api.instance().sockets.music.latency ?? "..."
|
||||||
}ms
|
}ms
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import { Button, Upload } from "antd"
|
import { Upload, Progress } from "antd"
|
||||||
|
import classnames from "classnames"
|
||||||
import { Icons } from "components/Icons"
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
|
import useHacks from "hooks/useHacks"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
const [uploading, setUploading] = React.useState(false)
|
const [uploading, setUploading] = React.useState(false)
|
||||||
|
const [progess, setProgess] = React.useState(null)
|
||||||
|
|
||||||
const handleOnStart = (file_uid, file) => {
|
const handleOnStart = (file_uid, file) => {
|
||||||
if (typeof props.onStart === "function") {
|
if (typeof props.onStart === "function") {
|
||||||
@ -32,35 +37,37 @@ export default (props) => {
|
|||||||
|
|
||||||
const handleUpload = async (req) => {
|
const handleUpload = async (req) => {
|
||||||
setUploading(true)
|
setUploading(true)
|
||||||
|
setProgess(1)
|
||||||
|
|
||||||
handleOnStart(req.file.uid, req.file)
|
handleOnStart(req.file.uid, req.file)
|
||||||
|
|
||||||
const response = await app.cores.remoteStorage.uploadFile(req.file, {
|
await app.cores.remoteStorage.uploadFile(req.file, {
|
||||||
onProgress: (file, progress) => {
|
onProgress: (file, progress) => {
|
||||||
return handleOnProgress(file.uid, progress)
|
setProgess(progress)
|
||||||
}
|
handleOnProgress(file.uid, progress)
|
||||||
}).catch((err) => {
|
},
|
||||||
app.notification.new({
|
onError: (file, error) => {
|
||||||
title: "Could not upload file",
|
setProgess(null)
|
||||||
description: err
|
handleOnError(file.uid, error)
|
||||||
}, {
|
setUploading(false)
|
||||||
type: "error"
|
},
|
||||||
})
|
onFinish: (file, response) => {
|
||||||
|
if (typeof props.ctx?.onUpdateItem === "function") {
|
||||||
|
props.ctx.onUpdateItem(response.url)
|
||||||
|
}
|
||||||
|
|
||||||
return handleOnError(req.file.uid, err)
|
if (typeof props.onUploadDone === "function") {
|
||||||
|
props.onUploadDone(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploading(false)
|
||||||
|
handleOnSuccess(req.file.uid, response)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setProgess(null)
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (typeof props.ctx?.onUpdateItem === "function") {
|
|
||||||
props.ctx.onUpdateItem(response.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof props.onUploadDone === "function") {
|
|
||||||
await props.onUploadDone(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
setUploading(false)
|
|
||||||
|
|
||||||
return handleOnSuccess(req.file.uid, response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Upload
|
return <Upload
|
||||||
@ -69,25 +76,43 @@ export default (props) => {
|
|||||||
props.multiple ?? false
|
props.multiple ?? false
|
||||||
}
|
}
|
||||||
accept={
|
accept={
|
||||||
props.accept ?? "image/*"
|
props.accept ?? [
|
||||||
|
"image/*",
|
||||||
|
"video/*",
|
||||||
|
"audio/*",
|
||||||
|
]
|
||||||
}
|
}
|
||||||
progress={false}
|
progress={false}
|
||||||
fileList={[]}
|
fileList={[]}
|
||||||
>
|
className={classnames(
|
||||||
<Button
|
"uploadButton",
|
||||||
icon={props.icon ?? <Icons.Upload
|
{
|
||||||
style={{
|
["uploading"]: !!progess || uploading
|
||||||
margin: 0
|
|
||||||
}}
|
|
||||||
/>}
|
|
||||||
loading={uploading}
|
|
||||||
type={
|
|
||||||
props.type ?? "round"
|
|
||||||
}
|
}
|
||||||
>
|
)}
|
||||||
|
disabled={uploading}
|
||||||
|
>
|
||||||
|
<div className="uploadButton-content">
|
||||||
|
{
|
||||||
|
!progess && (props.icon ?? <Icons.Upload
|
||||||
|
style={{
|
||||||
|
margin: 0
|
||||||
|
}}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
progess && <Progress
|
||||||
|
type="circle"
|
||||||
|
percent={progess}
|
||||||
|
strokeWidth={20}
|
||||||
|
format={() => null}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
props.children ?? "Upload"
|
props.children ?? "Upload"
|
||||||
}
|
}
|
||||||
</Button>
|
</div>
|
||||||
</Upload>
|
</Upload>
|
||||||
}
|
}
|
71
packages/app/src/components/UploadButton/index.less
Normal file
71
packages/app/src/components/UploadButton/index.less
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
.uploadButton {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
background-color: var(--background-color-accent);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
padding: 5px 15px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
&.uploading {
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.uploadButton-content {
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-upload {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadButton-content {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 7px;
|
||||||
|
|
||||||
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
.ant-progress {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: 17px;
|
||||||
|
width: 17px;
|
||||||
|
|
||||||
|
.ant-progress-inner,
|
||||||
|
.ant-progress-circle {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -113,7 +113,7 @@ export const UserCard = React.forwardRef((props, ref) => {
|
|||||||
<div className="username">
|
<div className="username">
|
||||||
<div className="username_text">
|
<div className="username_text">
|
||||||
<h1>
|
<h1>
|
||||||
{user.fullName || user.username}
|
{user.public_name || user.username}
|
||||||
{user.verified && <Icons.verifiedBadge />}
|
{user.verified && <Icons.verifiedBadge />}
|
||||||
</h1>
|
</h1>
|
||||||
<span>
|
<span>
|
||||||
|
@ -256,8 +256,6 @@ html {
|
|||||||
|
|
||||||
outline: 1px solid var(--border-color);
|
outline: 1px solid var(--border-color);
|
||||||
|
|
||||||
filter: drop-shadow(0 0 20px var(--border-color));
|
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
|
@ -15,7 +15,6 @@ export { default as StepsForm } from "./StepsForm"
|
|||||||
export { default as SearchButton } from "./SearchButton"
|
export { default as SearchButton } from "./SearchButton"
|
||||||
export { default as Skeleton } from "./Skeleton"
|
export { default as Skeleton } from "./Skeleton"
|
||||||
export { default as Navigation } from "./Navigation"
|
export { default as Navigation } from "./Navigation"
|
||||||
export { default as ImageUploader } from "./ImageUploader"
|
|
||||||
export { default as ImageViewer } from "./ImageViewer"
|
export { default as ImageViewer } from "./ImageViewer"
|
||||||
export { default as Image } from "./Image"
|
export { default as Image } from "./Image"
|
||||||
export { default as LoadMore } from "./LoadMore"
|
export { default as LoadMore } from "./LoadMore"
|
||||||
|
@ -2,8 +2,8 @@ import Core from "evite/src/core"
|
|||||||
|
|
||||||
import createClient from "comty.js"
|
import createClient from "comty.js"
|
||||||
|
|
||||||
import measurePing from "comty.js/handlers/measurePing"
|
import request from "comty.js/request"
|
||||||
import request from "comty.js/handlers/request"
|
import measurePing from "comty.js/helpers/measurePing"
|
||||||
import useRequest from "comty.js/hooks/useRequest"
|
import useRequest from "comty.js/hooks/useRequest"
|
||||||
import { reconnectWebsockets, disconnectWebsockets } from "comty.js"
|
import { reconnectWebsockets, disconnectWebsockets } from "comty.js"
|
||||||
|
|
||||||
@ -13,11 +13,11 @@ export default class APICore extends Core {
|
|||||||
static bgColor = "coral"
|
static bgColor = "coral"
|
||||||
static textColor = "black"
|
static textColor = "black"
|
||||||
|
|
||||||
instance = null
|
client = null
|
||||||
|
|
||||||
public = {
|
public = {
|
||||||
instance: function () {
|
client: function () {
|
||||||
return this.instance
|
return this.client
|
||||||
}.bind(this),
|
}.bind(this),
|
||||||
customRequest: request,
|
customRequest: request,
|
||||||
listenEvent: this.listenEvent.bind(this),
|
listenEvent: this.listenEvent.bind(this),
|
||||||
@ -28,82 +28,45 @@ export default class APICore extends Core {
|
|||||||
disconnectWebsockets: disconnectWebsockets,
|
disconnectWebsockets: disconnectWebsockets,
|
||||||
}
|
}
|
||||||
|
|
||||||
listenEvent(key, handler, instance) {
|
listenEvent(key, handler, instance = "default") {
|
||||||
if (!this.instance.wsInstances[instance ?? "default"]) {
|
if (!this.client.sockets[instance]) {
|
||||||
console.error(`[API] Websocket instance ${instance} not found`)
|
console.error(`[API] Websocket instance ${instance} not found`)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.instance.wsInstances[instance ?? "default"].on(key, handler)
|
return this.client.sockets[instance].on(key, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
unlistenEvent(key, handler, instance) {
|
unlistenEvent(key, handler, instance = "default") {
|
||||||
if (!this.instance.wsInstances[instance ?? "default"]) {
|
if (!this.client.sockets[instance]) {
|
||||||
console.error(`[API] Websocket instance ${instance} not found`)
|
console.error(`[API] Websocket instance ${instance} not found`)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.instance.wsInstances[instance ?? "default"].off(key, handler)
|
return this.client.sockets[instance].off(key, handler)
|
||||||
}
|
|
||||||
|
|
||||||
pendingPingsFromInstance = {}
|
|
||||||
|
|
||||||
createPingIntervals() {
|
|
||||||
// Object.keys(this.instance.wsInstances).forEach((instance) => {
|
|
||||||
// this.console.debug(`[API] Creating ping interval for ${instance}`)
|
|
||||||
|
|
||||||
// if (this.instance.wsInstances[instance].pingInterval) {
|
|
||||||
// clearInterval(this.instance.wsInstances[instance].pingInterval)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// this.instance.wsInstances[instance].pingInterval = setInterval(() => {
|
|
||||||
// if (this.instance.wsInstances[instance].pendingPingTry && this.instance.wsInstances[instance].pendingPingTry > 3) {
|
|
||||||
// this.console.debug(`[API] Ping timeout for ${instance}`)
|
|
||||||
|
|
||||||
// return clearInterval(this.instance.wsInstances[instance].pingInterval)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const timeStart = Date.now()
|
|
||||||
|
|
||||||
// //this.console.debug(`[API] Ping ${instance}`, this.instance.wsInstances[instance].pendingPingTry)
|
|
||||||
|
|
||||||
// this.instance.wsInstances[instance].emit("ping", () => {
|
|
||||||
// this.instance.wsInstances[instance].latency = Date.now() - timeStart
|
|
||||||
|
|
||||||
// this.instance.wsInstances[instance].pendingPingTry = 0
|
|
||||||
// })
|
|
||||||
|
|
||||||
// this.instance.wsInstances[instance].pendingPingTry = this.instance.wsInstances[instance].pendingPingTry ? this.instance.wsInstances[instance].pendingPingTry + 1 : 1
|
|
||||||
// }, 5000)
|
|
||||||
|
|
||||||
// // clear interval on close
|
|
||||||
// this.instance.wsInstances[instance].on("close", () => {
|
|
||||||
// clearInterval(this.instance.wsInstances[instance].pingInterval)
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onInitialize() {
|
async onInitialize() {
|
||||||
this.instance = await createClient({
|
this.client = await createClient({
|
||||||
enableWs: true,
|
enableWs: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.instance.eventBus.on("auth:login_success", () => {
|
this.client.eventBus.on("auth:login_success", () => {
|
||||||
app.eventBus.emit("auth:login_success")
|
app.eventBus.emit("auth:login_success")
|
||||||
})
|
})
|
||||||
|
|
||||||
this.instance.eventBus.on("auth:logout_success", () => {
|
this.client.eventBus.on("auth:logout_success", () => {
|
||||||
app.eventBus.emit("auth:logout_success")
|
app.eventBus.emit("auth:logout_success")
|
||||||
})
|
})
|
||||||
|
|
||||||
this.instance.eventBus.on("session.invalid", (error) => {
|
this.client.eventBus.on("session.invalid", (error) => {
|
||||||
app.eventBus.emit("session.invalid", error)
|
app.eventBus.emit("session.invalid", error)
|
||||||
})
|
})
|
||||||
|
|
||||||
// make a basic request to check if the API is available
|
// make a basic request to check if the API is available
|
||||||
await this.instance.instances["default"]({
|
await this.client.baseRequest({
|
||||||
method: "head",
|
method: "head",
|
||||||
url: "/",
|
url: "/",
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
@ -115,10 +78,6 @@ export default class APICore extends Core {
|
|||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.console.debug("[API] Attached to", this.instance)
|
return this.client
|
||||||
|
|
||||||
//this.createPingIntervals()
|
|
||||||
|
|
||||||
return this.instance
|
|
||||||
}
|
}
|
||||||
}
|
}
|
44
packages/app/src/cores/notifications/feedback.js
Normal file
44
packages/app/src/cores/notifications/feedback.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Haptics } from "@capacitor/haptics"
|
||||||
|
|
||||||
|
const NotfTypeToAudio = {
|
||||||
|
info: "notification",
|
||||||
|
success: "notification",
|
||||||
|
warning: "warn",
|
||||||
|
error: "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationFeedback {
|
||||||
|
static getSoundVolume = () => {
|
||||||
|
return (window.app.cores.settings.get("notifications_sound_volume") ?? 50) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
static playHaptic = async (options = {}) => {
|
||||||
|
const vibrationEnabled = options.vibrationEnabled ?? window.app.cores.settings.get("notifications_vibrate")
|
||||||
|
|
||||||
|
if (vibrationEnabled) {
|
||||||
|
await Haptics.vibrate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static playAudio = (options = {}) => {
|
||||||
|
const soundEnabled = options.soundEnabled ?? window.app.cores.settings.get("notifications_sound")
|
||||||
|
const soundVolume = options.soundVolume ? options.soundVolume / 100 : NotificationFeedback.getSoundVolume()
|
||||||
|
|
||||||
|
if (soundEnabled) {
|
||||||
|
if (typeof window.app.cores.sound?.play === "function") {
|
||||||
|
const sound = options.sound ?? NotfTypeToAudio[options.type] ?? "notification"
|
||||||
|
|
||||||
|
window.app.cores.sound.play(sound, {
|
||||||
|
volume: soundVolume,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async feedback(type) {
|
||||||
|
NotificationFeedback.playHaptic(type)
|
||||||
|
NotificationFeedback.playAudio(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotificationFeedback
|
44
packages/app/src/cores/notifications/notifications.core.js
Executable file
44
packages/app/src/cores/notifications/notifications.core.js
Executable file
@ -0,0 +1,44 @@
|
|||||||
|
import Core from "evite/src/core"
|
||||||
|
|
||||||
|
import NotificationUI from "./ui"
|
||||||
|
import NotificationFeedback from "./feedback"
|
||||||
|
|
||||||
|
export default class NotificationCore extends Core {
|
||||||
|
static namespace = "notifications"
|
||||||
|
static depenpencies = [
|
||||||
|
"api",
|
||||||
|
"settings",
|
||||||
|
]
|
||||||
|
|
||||||
|
#newNotifications = []
|
||||||
|
|
||||||
|
onEvents = {
|
||||||
|
"changeNotificationsSoundVolume": (value) => {
|
||||||
|
NotificationFeedback.playAudio({
|
||||||
|
soundVolume: value
|
||||||
|
})
|
||||||
|
},
|
||||||
|
"changeNotificationsVibrate": (value) => {
|
||||||
|
NotificationFeedback.playHaptic({
|
||||||
|
vibrationEnabled: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listenSockets = {
|
||||||
|
"notifications": {
|
||||||
|
"notification.new": (data) => {
|
||||||
|
this.new(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public = {
|
||||||
|
new: this.new,
|
||||||
|
}
|
||||||
|
|
||||||
|
async new(notification, options = {}) {
|
||||||
|
await NotificationUI.notify(notification, options)
|
||||||
|
await NotificationFeedback.feedback(options.type)
|
||||||
|
}
|
||||||
|
}
|
65
packages/app/src/cores/notifications/notifications.core.jsx → packages/app/src/cores/notifications/ui.jsx
Executable file → Normal file
65
packages/app/src/cores/notifications/notifications.core.jsx → packages/app/src/cores/notifications/ui.jsx
Executable file → Normal file
@ -1,46 +1,10 @@
|
|||||||
import Core from "evite/src/core"
|
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { notification as Notf, Space, Button } from "antd"
|
import { notification as Notf, Space, Button } from "antd"
|
||||||
import { Icons, createIconRender } from "components/Icons"
|
import { Icons, createIconRender } from "components/Icons"
|
||||||
import { Translation } from "react-i18next"
|
import { Translation } from "react-i18next"
|
||||||
import { Haptics } from "@capacitor/haptics"
|
|
||||||
|
|
||||||
const NotfTypeToAudio = {
|
class NotificationUI {
|
||||||
info: "notification",
|
static async notify(
|
||||||
success: "notification",
|
|
||||||
warning: "warn",
|
|
||||||
error: "error",
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class NotificationCore extends Core {
|
|
||||||
static namespace = "notifications"
|
|
||||||
|
|
||||||
onEvents = {
|
|
||||||
"changeNotificationsSoundVolume": (value) => {
|
|
||||||
this.playAudio({ soundVolume: value })
|
|
||||||
},
|
|
||||||
"changeNotificationsVibrate": (value) => {
|
|
||||||
this.playHaptic({
|
|
||||||
vibrationEnabled: value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registerToApp = {
|
|
||||||
notification: this
|
|
||||||
}
|
|
||||||
|
|
||||||
getSoundVolume = () => {
|
|
||||||
return (window.app.cores.settings.get("notifications_sound_volume") ?? 50) / 100
|
|
||||||
}
|
|
||||||
|
|
||||||
new = (notification, options = {}) => {
|
|
||||||
this.notify(notification, options)
|
|
||||||
this.playHaptic(options)
|
|
||||||
this.playAudio(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
notify(
|
|
||||||
notification,
|
notification,
|
||||||
options = {
|
options = {
|
||||||
type: "info"
|
type: "info"
|
||||||
@ -142,27 +106,6 @@ export default class NotificationCore extends Core {
|
|||||||
|
|
||||||
return Notf[options.type](notfObj)
|
return Notf[options.type](notfObj)
|
||||||
}
|
}
|
||||||
|
|
||||||
playHaptic = async (options = {}) => {
|
|
||||||
const vibrationEnabled = options.vibrationEnabled ?? window.app.cores.settings.get("notifications_vibrate")
|
|
||||||
|
|
||||||
if (vibrationEnabled) {
|
|
||||||
await Haptics.vibrate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playAudio = (options = {}) => {
|
|
||||||
const soundEnabled = options.soundEnabled ?? window.app.cores.settings.get("notifications_sound")
|
|
||||||
const soundVolume = options.soundVolume ? options.soundVolume / 100 : this.getSoundVolume()
|
|
||||||
|
|
||||||
if (soundEnabled) {
|
|
||||||
if (typeof window.app.cores.sound?.play === "function") {
|
|
||||||
const sound = options.sound ?? NotfTypeToAudio[options.type] ?? "notification"
|
|
||||||
|
|
||||||
window.app.cores.sound.play(sound, {
|
|
||||||
volume: soundVolume,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default NotificationUI
|
186
packages/app/src/cores/remoteStorage/chunkedUpload.js
Normal file
186
packages/app/src/cores/remoteStorage/chunkedUpload.js
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
import EventBus from "evite/src/internals/eventBus"
|
||||||
|
import SessionModel from "models/session"
|
||||||
|
|
||||||
|
export default class ChunkedUpload {
|
||||||
|
constructor(params) {
|
||||||
|
this.endpoint = params.endpoint
|
||||||
|
this.file = params.file
|
||||||
|
this.headers = params.headers || {}
|
||||||
|
this.postParams = params.postParams
|
||||||
|
this.service = params.service ?? "default"
|
||||||
|
this.retries = params.retries ?? app.cores.settings.get("uploader.retries") ?? 3
|
||||||
|
this.delayBeforeRetry = params.delayBeforeRetry || 5
|
||||||
|
|
||||||
|
this.start = 0
|
||||||
|
this.chunk = null
|
||||||
|
this.chunkCount = 0
|
||||||
|
|
||||||
|
this.splitChunkSize = params.splitChunkSize || 1024 * 1024 * 10
|
||||||
|
this.totalChunks = Math.ceil(this.file.size / this.splitChunkSize)
|
||||||
|
|
||||||
|
this.retriesCount = 0
|
||||||
|
this.offline = false
|
||||||
|
this.paused = false
|
||||||
|
|
||||||
|
this.headers["Authorization"] = `Bearer ${SessionModel.token}`
|
||||||
|
this.headers["uploader-original-name"] = encodeURIComponent(this.file.name)
|
||||||
|
this.headers["uploader-file-id"] = this.uniqid(this.file)
|
||||||
|
this.headers["uploader-chunks-total"] = this.totalChunks
|
||||||
|
this.headers["provider-type"] = this.service
|
||||||
|
this.headers["chunk-size"] = this.splitChunkSize
|
||||||
|
|
||||||
|
this._reader = new FileReader()
|
||||||
|
this.eventBus = new EventBus()
|
||||||
|
|
||||||
|
this.validateParams()
|
||||||
|
this.nextSend()
|
||||||
|
|
||||||
|
console.debug("[Uploader] Created", {
|
||||||
|
splitChunkSize: this.splitChunkSize,
|
||||||
|
totalChunks: this.totalChunks,
|
||||||
|
totalSize: this.file.size,
|
||||||
|
})
|
||||||
|
|
||||||
|
// restart sync when back online
|
||||||
|
// trigger events when offline/back online
|
||||||
|
window.addEventListener("online", () => {
|
||||||
|
if (!this.offline) return
|
||||||
|
|
||||||
|
this.offline = false
|
||||||
|
this.eventBus.emit("online")
|
||||||
|
this.nextSend()
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener("offline", () => {
|
||||||
|
this.offline = true
|
||||||
|
this.eventBus.emit("offline")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, fn) {
|
||||||
|
this.eventBus.on(event, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
validateParams() {
|
||||||
|
if (!this.endpoint || !this.endpoint.length) throw new TypeError("endpoint must be defined")
|
||||||
|
if (this.file instanceof File === false) throw new TypeError("file must be a File object")
|
||||||
|
if (this.headers && typeof this.headers !== "object") throw new TypeError("headers must be null or an object")
|
||||||
|
if (this.postParams && typeof this.postParams !== "object") throw new TypeError("postParams must be null or an object")
|
||||||
|
if (this.splitChunkSize && (typeof this.splitChunkSize !== "number" || this.splitChunkSize === 0)) throw new TypeError("splitChunkSize must be a positive number")
|
||||||
|
if (this.retries && (typeof this.retries !== "number" || this.retries === 0)) throw new TypeError("retries must be a positive number")
|
||||||
|
if (this.delayBeforeRetry && (typeof this.delayBeforeRetry !== "number")) throw new TypeError("delayBeforeRetry must be a positive number")
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqid(file) {
|
||||||
|
return Math.floor(Math.random() * 100000000) + Date.now() + this.file.size + "_tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
loadChunk() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const length = this.totalChunks === 1 ? this.file.size : this.splitChunkSize
|
||||||
|
const start = length * this.chunkCount
|
||||||
|
|
||||||
|
this._reader.onload = () => {
|
||||||
|
this.chunk = new Blob([this._reader.result], { type: "application/octet-stream" })
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
this._reader.readAsArrayBuffer(this.file.slice(start, start + length))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sendChunk() {
|
||||||
|
const form = new FormData()
|
||||||
|
|
||||||
|
// send post fields on last request
|
||||||
|
if (this.chunkCount + 1 === this.totalChunks && this.postParams) Object.keys(this.postParams).forEach(key => form.append(key, this.postParams[key]))
|
||||||
|
|
||||||
|
form.append("file", this.chunk)
|
||||||
|
|
||||||
|
this.headers["uploader-chunk-number"] = this.chunkCount
|
||||||
|
|
||||||
|
return fetch(this.endpoint, { method: "POST", headers: this.headers, body: form })
|
||||||
|
}
|
||||||
|
|
||||||
|
manageRetries() {
|
||||||
|
if (this.retriesCount++ < this.retries) {
|
||||||
|
setTimeout(() => this.nextSend(), this.delayBeforeRetry * 1000)
|
||||||
|
|
||||||
|
this.eventBus.emit("fileRetry", {
|
||||||
|
message: `An error occured uploading chunk ${this.chunkCount}. ${this.retries - this.retriesCount} retries left`,
|
||||||
|
chunk: this.chunkCount,
|
||||||
|
retriesLeft: this.retries - this.retriesCount
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventBus.emit("error", {
|
||||||
|
message: `An error occured uploading chunk ${this.chunkCount}. No more retries, stopping upload`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async nextSend() {
|
||||||
|
if (this.paused || this.offline) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loadChunk()
|
||||||
|
const res = await this.sendChunk()
|
||||||
|
.catch((err) => {
|
||||||
|
if (this.paused || this.offline) return
|
||||||
|
|
||||||
|
this.console.error(err)
|
||||||
|
|
||||||
|
// this type of error can happen after network disconnection on CORS setup
|
||||||
|
this.manageRetries()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status === 200 || res.status === 201 || res.status === 204) {
|
||||||
|
if (++this.chunkCount < this.totalChunks) {
|
||||||
|
this.nextSend()
|
||||||
|
} else {
|
||||||
|
res.json().then((body) => {
|
||||||
|
this.eventBus.emit("finish", body)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const percentProgress = Math.round((100 / this.totalChunks) * this.chunkCount)
|
||||||
|
|
||||||
|
this.eventBus.emit("progress", {
|
||||||
|
percentProgress
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// errors that might be temporary, wait a bit then retry
|
||||||
|
else if ([408, 502, 503, 504].includes(res.status)) {
|
||||||
|
if (this.paused || this.offline) return
|
||||||
|
|
||||||
|
this.manageRetries()
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
if (this.paused || this.offline) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.json().then((body) => {
|
||||||
|
this.eventBus.emit("error", {
|
||||||
|
message: `[${res.status}] ${body.error ?? body.message}`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
this.eventBus.emit("error", {
|
||||||
|
message: `[${res.status}] ${res.statusText}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePause() {
|
||||||
|
this.paused = !this.paused
|
||||||
|
|
||||||
|
if (!this.paused) {
|
||||||
|
this.nextSend()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,171 +1,6 @@
|
|||||||
import Core from "evite/src/core"
|
import Core from "evite/src/core"
|
||||||
import EventBus from "evite/src/internals/eventBus"
|
|
||||||
import SessionModel from "models/session"
|
|
||||||
|
|
||||||
class ChunkedUpload {
|
import ChunkedUpload from "./chunkedUpload"
|
||||||
constructor(params) {
|
|
||||||
this.endpoint = params.endpoint
|
|
||||||
this.file = params.file
|
|
||||||
this.headers = params.headers || {}
|
|
||||||
this.postParams = params.postParams
|
|
||||||
this.chunkSize = params.chunkSize || 1000000
|
|
||||||
this.service = params.service ?? "default"
|
|
||||||
this.retries = params.retries ?? app.cores.settings.get("uploader.retries") ?? 3
|
|
||||||
this.delayBeforeRetry = params.delayBeforeRetry || 5
|
|
||||||
|
|
||||||
this.start = 0
|
|
||||||
this.chunk = null
|
|
||||||
this.chunkCount = 0
|
|
||||||
this.totalChunks = Math.ceil(this.file.size / this.chunkSize)
|
|
||||||
this.retriesCount = 0
|
|
||||||
this.offline = false
|
|
||||||
this.paused = false
|
|
||||||
|
|
||||||
this.headers["Authorization"] = SessionModel.token
|
|
||||||
this.headers["uploader-original-name"] = encodeURIComponent(this.file.name)
|
|
||||||
this.headers["uploader-file-id"] = this.uniqid(this.file)
|
|
||||||
this.headers["uploader-chunks-total"] = this.totalChunks
|
|
||||||
this.headers["provider-type"] = this.service
|
|
||||||
|
|
||||||
this._reader = new FileReader()
|
|
||||||
this.eventBus = new EventBus()
|
|
||||||
|
|
||||||
this.validateParams()
|
|
||||||
this.sendChunks()
|
|
||||||
|
|
||||||
// restart sync when back online
|
|
||||||
// trigger events when offline/back online
|
|
||||||
window.addEventListener("online", () => {
|
|
||||||
if (!this.offline) return
|
|
||||||
|
|
||||||
this.offline = false
|
|
||||||
this.eventBus.emit("online")
|
|
||||||
this.sendChunks()
|
|
||||||
})
|
|
||||||
|
|
||||||
window.addEventListener("offline", () => {
|
|
||||||
this.offline = true
|
|
||||||
this.eventBus.emit("offline")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
on(event, fn) {
|
|
||||||
this.eventBus.on(event, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
validateParams() {
|
|
||||||
if (!this.endpoint || !this.endpoint.length) throw new TypeError("endpoint must be defined")
|
|
||||||
if (this.file instanceof File === false) throw new TypeError("file must be a File object")
|
|
||||||
if (this.headers && typeof this.headers !== "object") throw new TypeError("headers must be null or an object")
|
|
||||||
if (this.postParams && typeof this.postParams !== "object") throw new TypeError("postParams must be null or an object")
|
|
||||||
if (this.chunkSize && (typeof this.chunkSize !== "number" || this.chunkSize === 0)) throw new TypeError("chunkSize must be a positive number")
|
|
||||||
if (this.retries && (typeof this.retries !== "number" || this.retries === 0)) throw new TypeError("retries must be a positive number")
|
|
||||||
if (this.delayBeforeRetry && (typeof this.delayBeforeRetry !== "number")) throw new TypeError("delayBeforeRetry must be a positive number")
|
|
||||||
}
|
|
||||||
|
|
||||||
uniqid(file) {
|
|
||||||
return Math.floor(Math.random() * 100000000) + Date.now() + this.file.size + "_tmp"
|
|
||||||
}
|
|
||||||
|
|
||||||
getChunk() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const length = this.totalChunks === 1 ? this.file.size : this.chunkSize * 1000 * 1000
|
|
||||||
const start = length * this.chunkCount
|
|
||||||
|
|
||||||
this._reader.onload = () => {
|
|
||||||
this.chunk = new Blob([this._reader.result], { type: "application/octet-stream" })
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
this._reader.readAsArrayBuffer(this.file.slice(start, start + length))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sendChunk() {
|
|
||||||
const form = new FormData()
|
|
||||||
|
|
||||||
// send post fields on last request
|
|
||||||
if (this.chunkCount + 1 === this.totalChunks && this.postParams) Object.keys(this.postParams).forEach(key => form.append(key, this.postParams[key]))
|
|
||||||
|
|
||||||
form.append("file", this.chunk)
|
|
||||||
|
|
||||||
this.headers["uploader-chunk-number"] = this.chunkCount
|
|
||||||
|
|
||||||
return fetch(this.endpoint, { method: "POST", headers: this.headers, body: form })
|
|
||||||
}
|
|
||||||
|
|
||||||
manageRetries() {
|
|
||||||
if (this.retriesCount++ < this.retries) {
|
|
||||||
setTimeout(() => this.sendChunks(), this.delayBeforeRetry * 1000)
|
|
||||||
|
|
||||||
this.eventBus.emit("fileRetry", {
|
|
||||||
message: `An error occured uploading chunk ${this.chunkCount}. ${this.retries - this.retriesCount} retries left`,
|
|
||||||
chunk: this.chunkCount,
|
|
||||||
retriesLeft: this.retries - this.retriesCount
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.eventBus.emit("error", {
|
|
||||||
message: `An error occured uploading chunk ${this.chunkCount}. No more retries, stopping upload`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
sendChunks() {
|
|
||||||
if (this.paused || this.offline) return
|
|
||||||
|
|
||||||
this.getChunk()
|
|
||||||
.then(() => this.sendChunk())
|
|
||||||
.then((res) => {
|
|
||||||
if (res.status === 200 || res.status === 201 || res.status === 204) {
|
|
||||||
if (++this.chunkCount < this.totalChunks) this.sendChunks()
|
|
||||||
else {
|
|
||||||
res.json().then((body) => {
|
|
||||||
this.eventBus.emit("finish", body)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const percentProgress = Math.round((100 / this.totalChunks) * this.chunkCount)
|
|
||||||
|
|
||||||
this.eventBus.emit("progress", {
|
|
||||||
percentProgress
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// errors that might be temporary, wait a bit then retry
|
|
||||||
else if ([408, 502, 503, 504].includes(res.status)) {
|
|
||||||
if (this.paused || this.offline) return
|
|
||||||
|
|
||||||
this.manageRetries()
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
if (this.paused || this.offline) return
|
|
||||||
|
|
||||||
this.eventBus.emit("error", {
|
|
||||||
message: `An error occured uploading chunk ${this.chunkCount}. Server responded with ${res.status}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (this.paused || this.offline) return
|
|
||||||
|
|
||||||
this.console.error(err)
|
|
||||||
|
|
||||||
// this type of error can happen after network disconnection on CORS setup
|
|
||||||
this.manageRetries()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
togglePause() {
|
|
||||||
this.paused = !this.paused
|
|
||||||
|
|
||||||
if (!this.paused) {
|
|
||||||
this.sendChunks()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class RemoteStorage extends Core {
|
export default class RemoteStorage extends Core {
|
||||||
static namespace = "remoteStorage"
|
static namespace = "remoteStorage"
|
||||||
@ -190,19 +25,15 @@ export default class RemoteStorage extends Core {
|
|||||||
onProgress = () => { },
|
onProgress = () => { },
|
||||||
onFinish = () => { },
|
onFinish = () => { },
|
||||||
onError = () => { },
|
onError = () => { },
|
||||||
service = "default",
|
service = "standard",
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
const apiEndpoint = app.cores.api.instance().instances.files.getUri()
|
|
||||||
|
|
||||||
// TODO: get value from settings
|
|
||||||
const chunkSize = 2 * 1000 * 1000 // 10MB
|
|
||||||
|
|
||||||
return new Promise((_resolve, _reject) => {
|
return new Promise((_resolve, _reject) => {
|
||||||
const fn = async () => new Promise((resolve, reject) => {
|
const fn = async () => new Promise((resolve, reject) => {
|
||||||
const uploader = new ChunkedUpload({
|
const uploader = new ChunkedUpload({
|
||||||
endpoint: `${apiEndpoint}/upload/chunk`,
|
endpoint: `${app.cores.api.client().mainOrigin}/upload/chunk`,
|
||||||
chunkSize: chunkSize,
|
// TODO: get chunk size from settings
|
||||||
|
splitChunkSize: 5 * 1024 * 1024, // 5MB in bytes
|
||||||
file: file,
|
file: file,
|
||||||
service: service,
|
service: service,
|
||||||
})
|
})
|
||||||
@ -210,6 +41,13 @@ export default class RemoteStorage extends Core {
|
|||||||
uploader.on("error", ({ message }) => {
|
uploader.on("error", ({ message }) => {
|
||||||
this.console.error("[Uploader] Error", message)
|
this.console.error("[Uploader] Error", message)
|
||||||
|
|
||||||
|
app.notification.new({
|
||||||
|
title: "Could not upload file",
|
||||||
|
description: message
|
||||||
|
}, {
|
||||||
|
type: "error"
|
||||||
|
})
|
||||||
|
|
||||||
if (typeof onError === "function") {
|
if (typeof onError === "function") {
|
||||||
onError(file, message)
|
onError(file, message)
|
||||||
}
|
}
|
||||||
@ -219,8 +57,6 @@ export default class RemoteStorage extends Core {
|
|||||||
})
|
})
|
||||||
|
|
||||||
uploader.on("progress", ({ percentProgress }) => {
|
uploader.on("progress", ({ percentProgress }) => {
|
||||||
//this.console.debug(`[Uploader] Progress: ${percentProgress}%`)
|
|
||||||
|
|
||||||
if (typeof onProgress === "function") {
|
if (typeof onProgress === "function") {
|
||||||
onProgress(file, percentProgress)
|
onProgress(file, percentProgress)
|
||||||
}
|
}
|
||||||
@ -229,6 +65,12 @@ export default class RemoteStorage extends Core {
|
|||||||
uploader.on("finish", (data) => {
|
uploader.on("finish", (data) => {
|
||||||
this.console.debug("[Uploader] Finish", data)
|
this.console.debug("[Uploader] Finish", data)
|
||||||
|
|
||||||
|
app.notification.new({
|
||||||
|
title: "File uploaded",
|
||||||
|
}, {
|
||||||
|
type: "success"
|
||||||
|
})
|
||||||
|
|
||||||
if (typeof onFinish === "function") {
|
if (typeof onFinish === "function") {
|
||||||
onFinish(file, data)
|
onFinish(file, data)
|
||||||
}
|
}
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
import Core from "evite/src/core"
|
|
||||||
import socketio from "socket.io-client"
|
|
||||||
import remotes from "comty.js/remotes"
|
|
||||||
import SessionModel from "comty.js/models/session"
|
|
||||||
|
|
||||||
export default class RoomsController extends Core {
|
|
||||||
static namespace = "rooms"
|
|
||||||
|
|
||||||
connectedRooms = []
|
|
||||||
|
|
||||||
connectToRoom = async (roomId) => {
|
|
||||||
if (!this.checkRoomExists(roomId)) {
|
|
||||||
await this.createRoom(roomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const room = this.createRoomSocket(roomId)
|
|
||||||
|
|
||||||
this.connectedRooms.push(room)
|
|
||||||
|
|
||||||
return room
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectFromRoom = async (roomId) => {
|
|
||||||
if (!this.checkRoomExists(roomId)) {
|
|
||||||
throw new Error(`Room ${roomId} does not exist`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const room = this.connectedRooms.find((room) => room.roomId === roomId)
|
|
||||||
|
|
||||||
room.leave()
|
|
||||||
|
|
||||||
this.connectedRooms = this.connectedRooms.filter((room) => room.roomId !== roomId)
|
|
||||||
|
|
||||||
return room
|
|
||||||
}
|
|
||||||
|
|
||||||
checkRoomExists = (roomId) => {
|
|
||||||
return this.connectedRooms.some((room) => room.roomId === roomId)
|
|
||||||
}
|
|
||||||
|
|
||||||
createRoomSocket = async (roomId) => {
|
|
||||||
let roomInterface = {
|
|
||||||
roomId: roomId,
|
|
||||||
socket: socketio(remotes.chat.origin, {
|
|
||||||
transports: ["websocket"],
|
|
||||||
query: {
|
|
||||||
room: roomId,
|
|
||||||
},
|
|
||||||
auth: SessionModel.token,
|
|
||||||
autoConnect: true,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
room.leave = () => {
|
|
||||||
roomInterface.socket.disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
return roomInterface
|
|
||||||
}
|
|
||||||
}
|
|
@ -189,7 +189,7 @@ class MusicSyncSubCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onInitialize() {
|
async onInitialize() {
|
||||||
this.musicWs = this.ctx.CORES.api.instance.wsInstances.music
|
this.musicWs = this.ctx.CORES.api.instance.sockets.music
|
||||||
|
|
||||||
Object.keys(this.hubEvents).forEach((eventName) => {
|
Object.keys(this.hubEvents).forEach((eventName) => {
|
||||||
this.musicWs.on(eventName, this.hubEvents[eventName])
|
this.musicWs.on(eventName, this.hubEvents[eventName])
|
||||||
@ -371,6 +371,7 @@ class MusicSyncSubCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class SyncCore extends Core {
|
export default class SyncCore extends Core {
|
||||||
|
static disabled = true
|
||||||
static namespace = "sync"
|
static namespace = "sync"
|
||||||
static dependencies = ["api", "player"]
|
static dependencies = ["api", "player"]
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ export default class WidgetsCore extends Core {
|
|||||||
static storeKey = "widgets"
|
static storeKey = "widgets"
|
||||||
|
|
||||||
static get apiInstance() {
|
static get apiInstance() {
|
||||||
return app.cores.api.instance().instances.marketplace
|
return app.cores.api.client().baseRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
public = {
|
public = {
|
||||||
@ -21,7 +21,7 @@ export default class WidgetsCore extends Core {
|
|||||||
|
|
||||||
async onInitialize() {
|
async onInitialize() {
|
||||||
try {
|
try {
|
||||||
await WidgetsCore.apiInstance()
|
//await WidgetsCore.apiInstance()
|
||||||
|
|
||||||
const currentStore = this.getInstalled()
|
const currentStore = this.getInstalled()
|
||||||
|
|
||||||
|
29
packages/app/src/hooks/useUserRemoteConfig/index.jsx
Normal file
29
packages/app/src/hooks/useUserRemoteConfig/index.jsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import UserModel from "models/user"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export default (props = {}) => {
|
||||||
|
const [firstLoad, setFirstLoad] = React.useState(true)
|
||||||
|
const [localData, setLocalData] = React.useState({})
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
UserModel.getConfig().then((config) => {
|
||||||
|
setLocalData(config)
|
||||||
|
setFirstLoad(false)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
async function updateConfig(update) {
|
||||||
|
if (typeof props.onUpdate === "function") {
|
||||||
|
props.onUpdate(localData)
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await UserModel.updateConfig(update)
|
||||||
|
setLocalData(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
localData,
|
||||||
|
updateConfig,
|
||||||
|
firstLoad,
|
||||||
|
]
|
||||||
|
}
|
@ -58,19 +58,17 @@ class Modal extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleClickOutside = (e) => {
|
handleClickOutside = (e) => {
|
||||||
if (this.contentRef.current && !this.contentRef.current.contains(e.target)) {
|
if (this.props.confirmOnOutsideClick) {
|
||||||
if (this.props.confirmOnOutsideClick) {
|
return AntdModal.confirm({
|
||||||
return AntdModal.confirm({
|
title: this.props.confirmOnClickTitle ?? "Are you sure?",
|
||||||
title: this.props.confirmOnClickTitle ?? "Are you sure?",
|
content: this.props.confirmOnClickContent ?? "Are you sure you want to close this window?",
|
||||||
content: this.props.confirmOnClickContent ?? "Are you sure you want to close this window?",
|
onOk: () => {
|
||||||
onOk: () => {
|
this.close()
|
||||||
this.close()
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -82,14 +80,15 @@ class Modal extends React.Component {
|
|||||||
["framed"]: this.props.framed,
|
["framed"]: this.props.framed,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
onTouchEnd={this.handleClickOutside}
|
|
||||||
onMouseDown={this.handleClickOutside}
|
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
id="mask_trigger"
|
||||||
|
onTouchEnd={this.handleClickOutside}
|
||||||
|
onMouseDown={this.handleClickOutside}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className="app_modal_content"
|
className="app_modal_content"
|
||||||
ref={this.contentRef}
|
ref={this.contentRef}
|
||||||
onTouchEnd={this.handleClickOutside}
|
|
||||||
onMouseDown={this.handleClickOutside}
|
|
||||||
style={this.props.frameContentStyle}
|
style={this.props.frameContentStyle}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
@ -18,6 +18,17 @@
|
|||||||
|
|
||||||
transition: all 150ms ease-in-out;
|
transition: all 150ms ease-in-out;
|
||||||
|
|
||||||
|
#mask_trigger {
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
&.framed {
|
&.framed {
|
||||||
.app_modal_content {
|
.app_modal_content {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -2,9 +2,10 @@ import React from "react"
|
|||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import classnames from "classnames"
|
import classnames from "classnames"
|
||||||
import { Translation } from "react-i18next"
|
import { Translation } from "react-i18next"
|
||||||
|
import { motion, AnimatePresence } from "framer-motion"
|
||||||
|
|
||||||
import { Icons } from "components/Icons"
|
import { Icons } from "components/Icons"
|
||||||
import { Skeleton, FollowButton, UserCard } from "components"
|
import { FollowButton, UserCard } from "components"
|
||||||
import { SessionModel, UserModel, FollowsModel } from "models"
|
import { SessionModel, UserModel, FollowsModel } from "models"
|
||||||
|
|
||||||
import DetailsTab from "./tabs/details"
|
import DetailsTab from "./tabs/details"
|
||||||
@ -21,36 +22,6 @@ const TabsComponent = {
|
|||||||
"music": MusicTab,
|
"music": MusicTab,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TabRender = React.memo((props, ref) => {
|
|
||||||
const [transitionActive, setTransitionActive] = React.useState(false)
|
|
||||||
const [activeKey, setActiveKey] = React.useState(props.renderKey)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
setTransitionActive(true)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setActiveKey(props.renderKey)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setTransitionActive(false)
|
|
||||||
}, 100)
|
|
||||||
}, 100)
|
|
||||||
}, [props.renderKey])
|
|
||||||
|
|
||||||
const Tab = TabsComponent[activeKey]
|
|
||||||
|
|
||||||
if (!Tab) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// forwards ref to the tab
|
|
||||||
return <div className={classnames("fade-opacity-active", { "fade-opacity-leave": transitionActive })}>
|
|
||||||
{
|
|
||||||
React.createElement(Tab, props)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
|
|
||||||
export default class Account extends React.Component {
|
export default class Account extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
requestedUser: null,
|
requestedUser: null,
|
||||||
@ -66,16 +37,8 @@ export default class Account extends React.Component {
|
|||||||
isNotExistent: false,
|
isNotExistent: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
profileRef = React.createRef()
|
|
||||||
|
|
||||||
contentRef = React.createRef()
|
contentRef = React.createRef()
|
||||||
|
|
||||||
coverComponent = React.createRef()
|
|
||||||
|
|
||||||
leftPanelRef = React.createRef()
|
|
||||||
|
|
||||||
actionsRef = React.createRef()
|
|
||||||
|
|
||||||
componentDidMount = async () => {
|
componentDidMount = async () => {
|
||||||
app.layout.toggleCenteredContent(false)
|
app.layout.toggleCenteredContent(false)
|
||||||
|
|
||||||
@ -129,13 +92,6 @@ export default class Account extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onPostListTopVisibility = (to) => {
|
|
||||||
if (to) {
|
|
||||||
this.profileRef.current.classList.remove("topHidden")
|
|
||||||
} else {
|
|
||||||
this.profileRef.current.classList.add("topHidden")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickFollow = async () => {
|
onClickFollow = async () => {
|
||||||
const result = await FollowsModel.toggleFollow({
|
const result = await FollowsModel.toggleFollow({
|
||||||
@ -165,8 +121,6 @@ export default class Account extends React.Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onPostListTopVisibility(true)
|
|
||||||
|
|
||||||
key = key.toLowerCase()
|
key = key.toLowerCase()
|
||||||
|
|
||||||
if (this.state.tabActiveKey === key) {
|
if (this.state.tabActiveKey === key) {
|
||||||
@ -195,11 +149,10 @@ export default class Account extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
ref={this.profileRef}
|
|
||||||
className={classnames(
|
className={classnames(
|
||||||
"accountProfile",
|
"accountProfile",
|
||||||
{
|
{
|
||||||
["noCover"]: !user.cover,
|
["withCover"]: user.cover,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
id="profile"
|
id="profile"
|
||||||
@ -209,7 +162,6 @@ export default class Account extends React.Component {
|
|||||||
className={classnames("cover", {
|
className={classnames("cover", {
|
||||||
["expanded"]: this.state.coverExpanded
|
["expanded"]: this.state.coverExpanded
|
||||||
})}
|
})}
|
||||||
ref={this.coverComponent}
|
|
||||||
style={{ backgroundImage: `url("${user.cover}")` }}
|
style={{ backgroundImage: `url("${user.cover}")` }}
|
||||||
onClick={() => this.toggleCoverExpanded()}
|
onClick={() => this.toggleCoverExpanded()}
|
||||||
id="profile-cover"
|
id="profile-cover"
|
||||||
@ -217,18 +169,12 @@ export default class Account extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div className="panels">
|
<div className="panels">
|
||||||
<div
|
<div className="leftPanel">
|
||||||
className="leftPanel"
|
|
||||||
ref={this.leftPanelRef}
|
|
||||||
>
|
|
||||||
<UserCard
|
<UserCard
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div className="actions">
|
||||||
className="actions"
|
|
||||||
ref={this.actionsRef}
|
|
||||||
>
|
|
||||||
<FollowButton
|
<FollowButton
|
||||||
count={this.state.followersCount}
|
count={this.state.followersCount}
|
||||||
onClick={this.onClickFollow}
|
onClick={this.onClickFollow}
|
||||||
@ -239,17 +185,33 @@ export default class Account extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="content"
|
className="centerPanel"
|
||||||
ref={this.contentRef}
|
ref={this.contentRef}
|
||||||
>
|
>
|
||||||
<TabRender
|
<AnimatePresence mode="wait">
|
||||||
renderKey={this.state.tabActiveKey}
|
<motion.div
|
||||||
state={this.state}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
onTopVisibility={this.onPostListTopVisibility}
|
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>
|
||||||
|
|
||||||
<div className="tabMenuWrapper">
|
<div className="rightPanel">
|
||||||
<antd.Menu
|
<antd.Menu
|
||||||
className="tabMenu"
|
className="tabMenu"
|
||||||
mode={app.isMobile ? "horizontal" : "vertical"}
|
mode={app.isMobile ? "horizontal" : "vertical"}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
@import "theme/vars.less";
|
@import "theme/vars.less";
|
||||||
|
|
||||||
@borderRadius: 12px;
|
@borderRadius: 12px;
|
||||||
|
@stickyCardTop: 20px;
|
||||||
|
@withCoverPanelElevation: 100px;
|
||||||
|
|
||||||
.accountProfile {
|
.accountProfile {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -10,17 +14,20 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&.noCover {
|
&.withCover {
|
||||||
.panels {
|
.panels {
|
||||||
padding-top: 0;
|
|
||||||
|
|
||||||
.leftPanel {
|
.leftPanel {
|
||||||
transform: translate(0, 0) !important;
|
position: sticky;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.userCard {
|
top: calc(@withCoverPanelElevation + @stickyCardTop);
|
||||||
filter: none;
|
left: 0;
|
||||||
|
|
||||||
|
transform: translate(0, -@withCoverPanelElevation);
|
||||||
|
|
||||||
|
.userCard {
|
||||||
|
filter: drop-shadow(0 0 20px var(--border-color));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,14 +88,14 @@
|
|||||||
|
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
padding-top: 20px;
|
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.leftPanel {
|
.leftPanel {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: @stickyCardTop;
|
||||||
|
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
z-index: 55;
|
z-index: 55;
|
||||||
|
|
||||||
@ -99,14 +106,6 @@
|
|||||||
|
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
|
|
||||||
transform: translate(0, -100px);
|
|
||||||
|
|
||||||
.userCard {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -131,25 +130,51 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.centerPanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.fade-opacity-active {
|
.fade-opacity-active {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-list_wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.post-list {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
max-width: 900px;
|
||||||
|
|
||||||
|
.post_card {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
max-width: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabMenuWrapper {
|
.rightPanel {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: @stickyCardTop;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
background-color: var(--background-color-accent);
|
background-color: var(--background-color-accent);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px;
|
padding: 15px;
|
||||||
|
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
@ -158,6 +183,19 @@
|
|||||||
|
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
|
|
||||||
|
.ant-menu-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
padding: 5px 10px !important;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ant-menu-item-selected {
|
.ant-menu-item-selected {
|
||||||
background-color: var(--background-color-primary) !important;
|
background-color: var(--background-color-primary) !important;
|
||||||
}
|
}
|
||||||
@ -168,14 +206,12 @@
|
|||||||
background-color: var(--background-color-accent);
|
background-color: var(--background-color-accent);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: @borderRadius;
|
border-radius: @borderRadius;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1720px) {
|
@media (max-width: 1720px) {
|
||||||
.panels {
|
.panels {
|
||||||
.content {
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leftPanel {
|
.leftPanel {
|
||||||
.userCard {
|
.userCard {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
@ -188,12 +224,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabMenuWrapper {
|
.rightPanel {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
min-width: 0;
|
min-width: 60px;
|
||||||
|
|
||||||
top: 0;
|
align-items: center;
|
||||||
right: 0;
|
justify-content: center;
|
||||||
|
|
||||||
padding: 10px !important;
|
padding: 10px !important;
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ export default (props) => {
|
|||||||
|
|
||||||
<div className="field_value">
|
<div className="field_value">
|
||||||
<p>
|
<p>
|
||||||
{props.state.followers.length}
|
{props.state.followersCount}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -117,7 +117,7 @@ export default (props) => {
|
|||||||
<div className="field_value">
|
<div className="field_value">
|
||||||
<p>
|
<p>
|
||||||
{
|
{
|
||||||
getJoinLabel(Number(props.state.user.createdAt))
|
getJoinLabel(Number(props.state.user.created_at ?? props.state.user.createdAt))
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,7 +19,7 @@ export default (props) => {
|
|||||||
return <antd.Result
|
return <antd.Result
|
||||||
status="warning"
|
status="warning"
|
||||||
title="Failed to retrieve releases"
|
title="Failed to retrieve releases"
|
||||||
subTitle={E_Releases}
|
subTitle={E_Releases.message}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,19 +2,18 @@ import React from "react"
|
|||||||
|
|
||||||
import { PostsList } from "components"
|
import { PostsList } from "components"
|
||||||
|
|
||||||
import Post from "models/post"
|
import Feed from "models/feed"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
export default class ExplorePosts extends React.Component {
|
export default class ExplorePosts extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return <PostsList
|
return <PostsList
|
||||||
loadFromModel={Post.getExplorePosts}
|
loadFromModel={Feed.getGlobalTimelineFeed}
|
||||||
watchTimeline={[
|
watchTimeline={[
|
||||||
"post.new",
|
"post.new",
|
||||||
"post.delete",
|
"post.delete",
|
||||||
"feed.new",
|
"feed.new",
|
||||||
"feed.delete",
|
|
||||||
]}
|
]}
|
||||||
realtime
|
realtime
|
||||||
/>
|
/>
|
||||||
|
@ -16,8 +16,9 @@ const emptyListRender = () => {
|
|||||||
export class SavedPosts extends React.Component {
|
export class SavedPosts extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return <PostsList
|
return <PostsList
|
||||||
emptyListRender={emptyListRender}
|
|
||||||
loadFromModel={PostModel.getSavedPosts}
|
loadFromModel={PostModel.getSavedPosts}
|
||||||
|
emptyListRender={emptyListRender}
|
||||||
|
realtime={false}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
80
packages/app/src/pages/marketplace/index.jsx
Normal file
80
packages/app/src/pages/marketplace/index.jsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React from "react"
|
||||||
|
import SearchButton from "components/SearchButton"
|
||||||
|
import { Icons, createIconRender } from "components/Icons"
|
||||||
|
import Image from "components/Image"
|
||||||
|
|
||||||
|
import "./index.less"
|
||||||
|
|
||||||
|
const FieldItem = (props) => {
|
||||||
|
return <div className="marketplace-field-item">
|
||||||
|
<div className="marketplace-field-item-image">
|
||||||
|
<Image
|
||||||
|
src={props.image}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="marketplace-field-item-info">
|
||||||
|
<h1>
|
||||||
|
{props.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{props.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExtensionsBrowser = () => {
|
||||||
|
return <div className="marketplace-field">
|
||||||
|
<div className="marketplace-field-header">
|
||||||
|
<h1>
|
||||||
|
<Icons.MdCode />
|
||||||
|
Extensions
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="marketplace-field-slider">
|
||||||
|
<FieldItem
|
||||||
|
title="Example Extension"
|
||||||
|
description="Description"
|
||||||
|
image="https://placehold.co/400x400"
|
||||||
|
/>
|
||||||
|
<FieldItem
|
||||||
|
title="Example Extension"
|
||||||
|
description="Description"
|
||||||
|
image="https://placehold.co/400x400"
|
||||||
|
/>
|
||||||
|
<FieldItem
|
||||||
|
title="Example Extension"
|
||||||
|
description="Description bla blalbabla blalbabla blalbabla blalbabla blalbabla blalba"
|
||||||
|
image="https://placehold.co/400x400"
|
||||||
|
/>
|
||||||
|
<FieldItem
|
||||||
|
title="Bad image resolution"
|
||||||
|
description="Description"
|
||||||
|
image="https://placehold.co/1920x1080"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Marketplace = () => {
|
||||||
|
return <div className="marketplace">
|
||||||
|
<div className="marketplace-header">
|
||||||
|
<div className="marketplace-header-card">
|
||||||
|
<h1>
|
||||||
|
Marketplace
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ExtensionsBrowser />
|
||||||
|
<ExtensionsBrowser />
|
||||||
|
<ExtensionsBrowser />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Marketplace
|
114
packages/app/src/pages/marketplace/index.less
Normal file
114
packages/app/src/pages/marketplace/index.less
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
.marketplace {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 30px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.marketplace-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.marketplace-header-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
background-color: var(--background-color-accent);
|
||||||
|
padding: 10px 20px;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-family: "Space Grotesk", sans-serif;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.marketplace-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.marketplace-field-header {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.marketplace-field-slider {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marketplace-field-item {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
width: 250px;
|
||||||
|
height: 280px;
|
||||||
|
|
||||||
|
background-color: var(--background-color-accent);
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.marketplace-field-item-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 60%;
|
||||||
|
|
||||||
|
.lazy-load-image-background {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.marketplace-field-item-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 7px;
|
||||||
|
|
||||||
|
height: 40%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
h1, p {
|
||||||
|
text-wrap: none;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,44 +1,50 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
|
|
||||||
import Post from "models/post"
|
import PostCard from "components/PostCard"
|
||||||
import { PostCard, CommentsCard } from "components"
|
import PostsList from "components/PostsList"
|
||||||
|
|
||||||
|
import PostService from "models/post"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
export default (props) => {
|
export default (props) => {
|
||||||
const post_id = props.params.post_id
|
const post_id = props.params.post_id
|
||||||
|
|
||||||
const [data, setData] = React.useState(null)
|
const [loading, result, error, repeat] = app.cores.api.useRequest(PostService.getPost, {
|
||||||
|
post_id,
|
||||||
|
})
|
||||||
|
|
||||||
const loadData = async () => {
|
if (error) {
|
||||||
setData(null)
|
return <antd.Result
|
||||||
|
status="warning"
|
||||||
const data = await Post.getPost({ post_id }).catch(() => {
|
title="Failed to retrieve post"
|
||||||
antd.message.error("Failed to get post")
|
subTitle={error.message}
|
||||||
|
/>
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
setData(data)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
if (loading) {
|
||||||
loadData()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return <antd.Skeleton active />
|
return <antd.Skeleton active />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="postPage">
|
return <div className="post-page">
|
||||||
<div className="postWrapper">
|
<div className="post-page-original">
|
||||||
<PostCard data={data} fullmode />
|
<h1>Post</h1>
|
||||||
|
|
||||||
|
<PostCard
|
||||||
|
data={result}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="commentsWrapper">
|
|
||||||
<CommentsCard post_id={data._id} />
|
<div className="post-page-replies">
|
||||||
|
<h1>Replies</h1>
|
||||||
|
<PostsList
|
||||||
|
disableReplyTag
|
||||||
|
loadFromModel={PostService.replies}
|
||||||
|
loadFromModelProps={{
|
||||||
|
post_id,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
@ -1,30 +1,8 @@
|
|||||||
.postPage {
|
.post-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100vh;
|
|
||||||
|
|
||||||
overflow: hidden;
|
gap: 20px;
|
||||||
|
|
||||||
.postWrapper {
|
|
||||||
margin: 0 10px;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
width: 70vw;
|
|
||||||
|
|
||||||
min-width: 70vw;
|
|
||||||
max-width: 70vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commentsWrapper {
|
|
||||||
height: 100vh;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
min-width: 300px;
|
|
||||||
|
|
||||||
overflow: scroll;
|
|
||||||
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -308,6 +308,10 @@ export default class SettingItemComponent extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof this.props.onUpdate === "function") {
|
||||||
|
await this.props.onUpdate(updateValue)
|
||||||
|
}
|
||||||
|
|
||||||
// finaly update value
|
// finaly update value
|
||||||
await this.setState({
|
await this.setState({
|
||||||
value: updateValue
|
value: updateValue
|
||||||
|
@ -16,50 +16,49 @@ import SettingItemComponent from "../SettingItemComponent"
|
|||||||
export default class SettingTab extends React.Component {
|
export default class SettingTab extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
loading: true,
|
loading: true,
|
||||||
processedCtx: {}
|
tab: null,
|
||||||
|
ctx: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
tab = composedTabs[this.props.activeKey]
|
loadTab = async () => {
|
||||||
|
await this.setState({
|
||||||
|
loading: true,
|
||||||
|
processedCtx: {},
|
||||||
|
})
|
||||||
|
|
||||||
processCtx = async () => {
|
const tab = composedTabs[this.props.activeKey]
|
||||||
if (typeof this.tab.ctxData === "function") {
|
|
||||||
this.setState({ loading: true })
|
|
||||||
|
|
||||||
const resultCtx = await this.tab.ctxData()
|
let ctx = {}
|
||||||
|
|
||||||
console.log(resultCtx)
|
if (typeof tab.ctxData === "function") {
|
||||||
|
ctx = await tab.ctxData()
|
||||||
this.setState({
|
|
||||||
loading: false,
|
|
||||||
processedCtx: resultCtx
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.setState({
|
||||||
|
tab: tab,
|
||||||
|
loading: false,
|
||||||
|
ctx: {
|
||||||
|
baseConfig: this.props.baseConfig,
|
||||||
|
...ctx
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if props.activeKey change
|
// check if props.activeKey change
|
||||||
componentDidUpdate = async (prevProps) => {
|
componentDidUpdate = async (prevProps) => {
|
||||||
if (prevProps.activeKey !== this.props.activeKey) {
|
if (prevProps.activeKey !== this.props.activeKey) {
|
||||||
this.tab = composedTabs[this.props.activeKey]
|
await this.loadTab()
|
||||||
|
|
||||||
this.setState({
|
|
||||||
loading: !!this.tab.ctxData,
|
|
||||||
processedCtx: {}
|
|
||||||
})
|
|
||||||
|
|
||||||
await this.processCtx()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount = async () => {
|
componentDidMount = async () => {
|
||||||
this.setState({
|
await this.loadTab()
|
||||||
loading: !!this.tab.ctxData,
|
}
|
||||||
})
|
|
||||||
|
|
||||||
await this.processCtx()
|
handleSettingUpdate = async (key, value) => {
|
||||||
|
if (typeof this.props.onUpdate === "function") {
|
||||||
this.setState({
|
await this.props.onUpdate(key, value)
|
||||||
loading: false
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -67,14 +66,16 @@ export default class SettingTab extends React.Component {
|
|||||||
return <antd.Skeleton active />
|
return <antd.Skeleton active />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.tab.render) {
|
const { ctx, tab } = this.state
|
||||||
return React.createElement(this.tab.render, {
|
|
||||||
ctx: this.state.processedCtx
|
if (tab.render) {
|
||||||
|
return React.createElement(tab.render, {
|
||||||
|
ctx: ctx,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.withGroups) {
|
if (this.props.withGroups) {
|
||||||
const group = composeGroupsFromSettingsTab(this.tab.settings)
|
const group = composeGroupsFromSettingsTab(tab.settings)
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{
|
{
|
||||||
@ -98,9 +99,11 @@ export default class SettingTab extends React.Component {
|
|||||||
|
|
||||||
<div className="settings_list">
|
<div className="settings_list">
|
||||||
{
|
{
|
||||||
settings.map((setting) => <SettingItemComponent
|
settings.map((setting, index) => <SettingItemComponent
|
||||||
|
key={index}
|
||||||
setting={setting}
|
setting={setting}
|
||||||
ctx={this.state.processedCtx}
|
ctx={ctx}
|
||||||
|
onUpdate={(value) => this.handleSettingUpdate(setting.id, value)}
|
||||||
/>)
|
/>)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -109,8 +112,8 @@ export default class SettingTab extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
this.tab.footer && React.createElement(this.tab.footer, {
|
tab.footer && React.createElement(tab.footer, {
|
||||||
ctx: this.state.processedCtx
|
ctx: this.state.ctx
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
@ -118,18 +121,22 @@ export default class SettingTab extends React.Component {
|
|||||||
|
|
||||||
return <>
|
return <>
|
||||||
{
|
{
|
||||||
this.tab.settings.map((setting, index) => {
|
tab.settings.map((setting, index) => {
|
||||||
return <SettingItemComponent
|
return <SettingItemComponent
|
||||||
key={index}
|
key={index}
|
||||||
setting={setting}
|
setting={setting}
|
||||||
ctx={this.state.processedCtx}
|
ctx={{
|
||||||
|
...this.state.ctx,
|
||||||
|
baseConfig: this.props.baseConfig,
|
||||||
|
}}
|
||||||
|
onUpdate={(value) => this.handleSettingUpdate(setting.id, value)}
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
this.tab.footer && React.createElement(this.tab.footer, {
|
tab.footer && React.createElement(tab.footer, {
|
||||||
ctx: this.state.processedCtx
|
ctx: this.state.ctx
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as antd from "antd"
|
import * as antd from "antd"
|
||||||
import { Translation } from "react-i18next"
|
|
||||||
import classnames from "classnames"
|
|
||||||
import config from "config"
|
|
||||||
import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
|
|
||||||
|
|
||||||
import { createIconRender } from "components/Icons"
|
import { createIconRender } from "components/Icons"
|
||||||
|
import { Translation } from "react-i18next"
|
||||||
|
import config from "config"
|
||||||
|
|
||||||
|
import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
|
||||||
|
import useUserRemoteConfig from "hooks/useUserRemoteConfig"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
composedSettingsByGroups as settings
|
composedSettingsByGroups as settings
|
||||||
@ -88,6 +88,7 @@ const generateMenuItems = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
const [config, setConfig, loading] = useUserRemoteConfig()
|
||||||
const [activeKey, setActiveKey] = useUrlQueryActiveKey({
|
const [activeKey, setActiveKey] = useUrlQueryActiveKey({
|
||||||
defaultKey: "general",
|
defaultKey: "general",
|
||||||
queryKey: "tab"
|
queryKey: "tab"
|
||||||
@ -113,11 +114,14 @@ export default () => {
|
|||||||
return items
|
return items
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div
|
function handleOnUpdate(key, value) {
|
||||||
className={classnames(
|
setConfig({
|
||||||
"settings_wrapper",
|
...config,
|
||||||
)}
|
[key]: value
|
||||||
>
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="settings_wrapper">
|
||||||
<div className="settings_menu">
|
<div className="settings_menu">
|
||||||
<antd.Menu
|
<antd.Menu
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
@ -128,10 +132,17 @@ export default () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings_content">
|
<div className="settings_content">
|
||||||
<SettingTab
|
{
|
||||||
activeKey={activeKey}
|
loading && <antd.Skeleton active />
|
||||||
withGroups
|
}
|
||||||
/>
|
{
|
||||||
|
!loading && <SettingTab
|
||||||
|
baseConfig={config}
|
||||||
|
onUpdate={handleOnUpdate}
|
||||||
|
activeKey={activeKey}
|
||||||
|
withGroups
|
||||||
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
@ -95,6 +95,11 @@
|
|||||||
|
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
|
||||||
|
.uploadButton{
|
||||||
|
background-color: var(--background-color-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
.setting_item_header {
|
.setting_item_header {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -299,23 +299,100 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-notification-notice {
|
.ant-notification {
|
||||||
background-color: var(--background-color-primary) !important;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.ant-notification-notice-wrapper {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
outline: 1px solid var(--border-color) !important;
|
||||||
|
background-color: var(--background-color-primary) !important;
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6,
|
|
||||||
p,
|
|
||||||
span {
|
|
||||||
color: var(--text-color) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-notification-notice-message,
|
.ant-notification-notice {
|
||||||
.ant-notification-notice-description {
|
display: flex;
|
||||||
color: var(--text-color) !important;
|
flex-direction: column;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
background-color: transparent !important;
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
span {
|
||||||
|
color: var(--text-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-notification-notice-close-x {
|
||||||
|
svg {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-notification-notice-with-icon {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.ant-notification-notice-message {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-notification-notice-message,
|
||||||
|
.ant-notification-notice-description {
|
||||||
|
color: var(--text-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-notification-notice-description {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-notification-notice-description:not(:empty) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-notification-notice-with-icon {
|
||||||
|
.ant-notification-notice-message {
|
||||||
|
margin-inline-start: 46px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-notification-notice-description {
|
||||||
|
margin-inline-start: 46px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-notification-notice-icon {
|
||||||
|
&.ant-notification-notice-icon-success {
|
||||||
|
color: #52c41a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
max-width: 40px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,6 +411,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.ant-message-error {
|
.ant-message-error {
|
||||||
svg {
|
svg {
|
||||||
color: var(--ant-error-color) !important;
|
color: var(--ant-error-color) !important;
|
||||||
@ -518,30 +596,6 @@
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-notification-notice {
|
|
||||||
.ant-notification-notice-with-icon {
|
|
||||||
.ant-notification-notice-message {
|
|
||||||
margin-inline-start: 46px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-notification-notice-description {
|
|
||||||
margin-inline-start: 46px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-notification-notice-icon {
|
|
||||||
max-width: 40px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.adm-action-sheet {
|
.adm-action-sheet {
|
||||||
.adm-action-sheet-button-item-wrapper {
|
.adm-action-sheet-button-item-wrapper {
|
||||||
border-bottom-color: var(--border-color);
|
border-bottom-color: var(--border-color);
|
||||||
|
180
packages/server/classes/ChunkFileUpload/index.js
Executable file
180
packages/server/classes/ChunkFileUpload/index.js
Executable file
@ -0,0 +1,180 @@
|
|||||||
|
// Orginal forked from: Buzut/huge-uploader-nodejs
|
||||||
|
// Copyright (c) 2018, Quentin Busuttil All rights reserved.
|
||||||
|
|
||||||
|
import fs from "node:fs"
|
||||||
|
import path from "node:path"
|
||||||
|
import mimetypes from "mime-types"
|
||||||
|
|
||||||
|
export function checkTotalSize(
|
||||||
|
chunkSize, // in bytes
|
||||||
|
totalChunks, // number of chunks
|
||||||
|
maxFileSize, // in bytes
|
||||||
|
) {
|
||||||
|
const totalSize = chunkSize * totalChunks
|
||||||
|
|
||||||
|
if (totalSize > maxFileSize) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkChunkUploadHeaders(headers) {
|
||||||
|
if (
|
||||||
|
!headers["uploader-chunk-number"] ||
|
||||||
|
!headers["uploader-chunks-total"] ||
|
||||||
|
!headers["uploader-original-name"] ||
|
||||||
|
!headers["uploader-file-id"] ||
|
||||||
|
!headers["uploader-chunks-total"].match(/^[0-9]+$/) ||
|
||||||
|
!headers["uploader-chunk-number"].match(/^[0-9]+$/)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAssembleChunksPromise({
|
||||||
|
chunksPath, // chunks to assemble
|
||||||
|
filePath, // final assembled file path
|
||||||
|
maxFileSize,
|
||||||
|
}) {
|
||||||
|
return () => new Promise(async (resolve, reject) => {
|
||||||
|
let fileSize = 0
|
||||||
|
|
||||||
|
const chunks = await fs.promises.readdir(chunksPath)
|
||||||
|
|
||||||
|
if (chunks.length === 0) {
|
||||||
|
throw new Error("No chunks found")
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const chunk of chunks) {
|
||||||
|
const chunkPath = path.join(chunksPath, chunk)
|
||||||
|
const data = await fs.promises.readFile(chunkPath)
|
||||||
|
|
||||||
|
fileSize += data.length
|
||||||
|
|
||||||
|
// check if final file gonna exceed max file size
|
||||||
|
// in case early estimation is wrong (due client send bad headers)
|
||||||
|
if (fileSize > maxFileSize) {
|
||||||
|
return reject(new OperationError(413, "File exceeds max total file size, aborting assembly..."))
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.promises.appendFile(filePath, data)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve({
|
||||||
|
chunksLength: chunks.length,
|
||||||
|
filePath: filePath,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleChunkFile(fileStream, { tmpDir, headers, maxFileSize, maxChunkSize }) {
|
||||||
|
return await new Promise(async (resolve, reject) => {
|
||||||
|
const chunksPath = path.join(tmpDir, headers["uploader-file-id"], "chunks")
|
||||||
|
const chunkPath = path.join(chunksPath, headers["uploader-chunk-number"])
|
||||||
|
|
||||||
|
const chunkCount = +headers["uploader-chunk-number"]
|
||||||
|
const totalChunks = +headers["uploader-chunks-total"]
|
||||||
|
|
||||||
|
// check if file has all chunks uploaded
|
||||||
|
const isLast = chunkCount === totalChunks - 1
|
||||||
|
|
||||||
|
// make sure chunk is in range
|
||||||
|
if (chunkCount < 0 || chunkCount >= totalChunks) {
|
||||||
|
throw new Error("Chunk is out of range")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if is the first chunk check if dir exists before write things
|
||||||
|
if (chunkCount === 0) {
|
||||||
|
if (!await fs.promises.stat(chunksPath).catch(() => false)) {
|
||||||
|
await fs.promises.mkdir(chunksPath, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataWritten = 0
|
||||||
|
|
||||||
|
let writeStream = fs.createWriteStream(chunkPath)
|
||||||
|
|
||||||
|
writeStream.on("error", (err) => {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
writeStream.on("close", () => {
|
||||||
|
if (maxChunkSize !== undefined) {
|
||||||
|
if (dataWritten > maxChunkSize) {
|
||||||
|
reject(new OperationError(413, "Chunk size exceeds max chunk size, aborting upload..."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// estimate total file size,
|
||||||
|
// if estimation exceeds maxFileSize, abort upload
|
||||||
|
if (chunkCount === 0 && totalChunks > 0) {
|
||||||
|
if ((dataWritten * (totalChunks - 1)) > maxFileSize) {
|
||||||
|
reject(new OperationError(413, "File estimated size exceeds max total file size, aborting upload..."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLast) {
|
||||||
|
const mimetype = mimetypes.lookup(headers["uploader-original-name"])
|
||||||
|
const extension = mimetypes.extension(mimetype)
|
||||||
|
|
||||||
|
let filename = headers["uploader-file-id"]
|
||||||
|
|
||||||
|
if (headers["uploader-use-date"] === "true") {
|
||||||
|
filename = `${filename}_${Date.now()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(createAssembleChunksPromise({
|
||||||
|
// build data
|
||||||
|
chunksPath: chunksPath,
|
||||||
|
filePath: path.resolve(chunksPath, `${filename}.${extension}`),
|
||||||
|
maxFileSize: maxFileSize,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
fileStream.on("data", (buffer) => {
|
||||||
|
dataWritten += buffer.byteLength
|
||||||
|
})
|
||||||
|
|
||||||
|
fileStream.pipe(writeStream)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadChunkFile(req, {
|
||||||
|
tmpDir,
|
||||||
|
maxFileSize,
|
||||||
|
maxChunkSize,
|
||||||
|
}) {
|
||||||
|
return await new Promise(async (resolve, reject) => {
|
||||||
|
if (!checkChunkUploadHeaders(req.headers)) {
|
||||||
|
reject(new OperationErrorError(400, "Missing header(s)"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await req.multipart(async (field) => {
|
||||||
|
try {
|
||||||
|
const result = await handleChunkFile(field.file.stream, {
|
||||||
|
tmpDir: tmpDir,
|
||||||
|
headers: req.headers,
|
||||||
|
maxFileSize: maxFileSize,
|
||||||
|
maxChunkSize: maxChunkSize,
|
||||||
|
})
|
||||||
|
|
||||||
|
return resolve(result)
|
||||||
|
} catch (error) {
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default uploadChunkFile
|
@ -1,260 +0,0 @@
|
|||||||
// Orginal forked from: Buzut/huge-uploader-nodejs
|
|
||||||
// Copyright (c) 2018, Quentin Busuttil All rights reserved.
|
|
||||||
|
|
||||||
import fs from "node:fs"
|
|
||||||
import path from "node:path"
|
|
||||||
import { promisify } from "node:util"
|
|
||||||
import mimetypes from "mime-types"
|
|
||||||
import crypto from "node:crypto"
|
|
||||||
|
|
||||||
import Busboy from "busboy"
|
|
||||||
|
|
||||||
export function getFileHash(file) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const hash = crypto.createHash("sha256")
|
|
||||||
|
|
||||||
file.on("data", (chunk) => hash.update(chunk))
|
|
||||||
|
|
||||||
file.on("end", () => resolve(hash.digest("hex")))
|
|
||||||
|
|
||||||
file.on("error", reject)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkHeaders(headers) {
|
|
||||||
if (
|
|
||||||
!headers["uploader-chunk-number"] ||
|
|
||||||
!headers["uploader-chunks-total"] ||
|
|
||||||
!headers["uploader-original-name"] ||
|
|
||||||
!headers["uploader-file-id"] ||
|
|
||||||
!headers["uploader-chunks-total"].match(/^[0-9]+$/) ||
|
|
||||||
!headers["uploader-chunk-number"].match(/^[0-9]+$/)
|
|
||||||
) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkTotalSize(maxFileSize, maxChunkSize, totalChunks) {
|
|
||||||
if (maxChunkSize * totalChunks > maxFileSize) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cleanChunks(dirPath) {
|
|
||||||
fs.readdir(dirPath, (err, files) => {
|
|
||||||
let filesLength = files.length
|
|
||||||
|
|
||||||
files.forEach((file) => {
|
|
||||||
fs.unlink(path.join(dirPath, file), () => {
|
|
||||||
if (--filesLength === 0) fs.rmdir(dirPath, () => { }) // cb does nothing but required
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAssembleChunksPromise({
|
|
||||||
tmpDir,
|
|
||||||
headers,
|
|
||||||
useDate,
|
|
||||||
}) {
|
|
||||||
const asyncReadFile = promisify(fs.readFile)
|
|
||||||
const asyncAppendFile = promisify(fs.appendFile)
|
|
||||||
|
|
||||||
const originalMimeType = mimetypes.lookup(headers["uploader-original-name"])
|
|
||||||
const originalExtension = mimetypes.extension(originalMimeType)
|
|
||||||
|
|
||||||
const totalChunks = +headers["uploader-chunks-total"]
|
|
||||||
|
|
||||||
const fileId = headers["uploader-file-id"]
|
|
||||||
const workPath = path.join(tmpDir, fileId)
|
|
||||||
const chunksPath = path.resolve(workPath, "chunks")
|
|
||||||
const assembledFilepath = path.join(workPath, `assembled.${originalExtension}`)
|
|
||||||
|
|
||||||
let chunkCount = 0
|
|
||||||
let finalFilepath = null
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const onEnd = async () => {
|
|
||||||
try {
|
|
||||||
const hash = await getFileHash(fs.createReadStream(assembledFilepath))
|
|
||||||
|
|
||||||
if (useDate) {
|
|
||||||
finalFilepath = path.resolve(workPath, `${hash}_${Date.now()}.${originalExtension}`)
|
|
||||||
} else {
|
|
||||||
finalFilepath = path.resolve(workPath, `${hash}.${originalExtension}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.renameSync(assembledFilepath, finalFilepath)
|
|
||||||
|
|
||||||
cleanChunks(chunksPath)
|
|
||||||
|
|
||||||
return resolve({
|
|
||||||
filename: headers["uploader-original-name"],
|
|
||||||
filepath: finalFilepath,
|
|
||||||
cachePath: workPath,
|
|
||||||
hash,
|
|
||||||
mimetype: originalMimeType,
|
|
||||||
extension: originalExtension,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
return reject(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pipeChunk = () => {
|
|
||||||
asyncReadFile(path.join(chunksPath, chunkCount.toString()))
|
|
||||||
.then((chunk) => asyncAppendFile(assembledFilepath, chunk))
|
|
||||||
.then(() => {
|
|
||||||
// 0 indexed files = length - 1, so increment before comparison
|
|
||||||
if (totalChunks > ++chunkCount) {
|
|
||||||
return pipeChunk(chunkCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return onEnd()
|
|
||||||
})
|
|
||||||
.catch(reject)
|
|
||||||
}
|
|
||||||
|
|
||||||
pipeChunk()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mkdirIfDoesntExist(dirPath, callback) {
|
|
||||||
if (!fs.existsSync(dirPath)) {
|
|
||||||
fs.mkdir(dirPath, { recursive: true }, callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleFile(tmpDir, headers, fileStream) {
|
|
||||||
const dirPath = path.join(tmpDir, headers["uploader-file-id"])
|
|
||||||
const chunksPath = path.join(dirPath, "chunks")
|
|
||||||
const chunkPath = path.join(chunksPath, headers["uploader-chunk-number"])
|
|
||||||
const useDate = headers["uploader-use-date"] === "true"
|
|
||||||
const chunkCount = +headers["uploader-chunk-number"]
|
|
||||||
const totalChunks = +headers["uploader-chunks-total"]
|
|
||||||
|
|
||||||
let error
|
|
||||||
let assembleChunksPromise
|
|
||||||
let finished = false
|
|
||||||
let writeStream
|
|
||||||
|
|
||||||
const writeFile = () => {
|
|
||||||
writeStream = fs.createWriteStream(chunkPath)
|
|
||||||
|
|
||||||
writeStream.on("error", (err) => {
|
|
||||||
error = err
|
|
||||||
fileStream.resume()
|
|
||||||
})
|
|
||||||
|
|
||||||
writeStream.on("close", () => {
|
|
||||||
finished = true
|
|
||||||
|
|
||||||
// if all is uploaded
|
|
||||||
if (chunkCount === totalChunks - 1) {
|
|
||||||
assembleChunksPromise = createAssembleChunksPromise({
|
|
||||||
tmpDir,
|
|
||||||
headers,
|
|
||||||
useDate,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fileStream.pipe(writeStream)
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure chunk is in range
|
|
||||||
if (chunkCount < 0 || chunkCount >= totalChunks) {
|
|
||||||
error = new Error("Chunk is out of range")
|
|
||||||
fileStream.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (chunkCount === 0) {
|
|
||||||
// create file upload dir if it's first chunk
|
|
||||||
mkdirIfDoesntExist(chunksPath, (err) => {
|
|
||||||
if (err) {
|
|
||||||
error = err
|
|
||||||
fileStream.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
else writeFile()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
// make sure dir exists if it's not first chunk
|
|
||||||
fs.stat(dirPath, (err) => {
|
|
||||||
if (err) {
|
|
||||||
error = new Error("Upload has expired")
|
|
||||||
fileStream.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
else writeFile()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (callback) => {
|
|
||||||
if (finished && !error) callback(null, assembleChunksPromise)
|
|
||||||
else if (error) callback(error)
|
|
||||||
|
|
||||||
else {
|
|
||||||
writeStream.on("error", callback)
|
|
||||||
writeStream.on("close", () => callback(null, assembleChunksPromise))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uploadFile(req, tmpDir, maxFileSize, maxChunkSize) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!checkHeaders(req.headers)) {
|
|
||||||
reject(new Error("Missing header(s)"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkTotalSize(maxFileSize, req.headers["uploader-chunks-total"])) {
|
|
||||||
reject(new Error("File is above size limit"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let limitReached = false
|
|
||||||
let getFileStatus
|
|
||||||
|
|
||||||
const busboy = Busboy({ headers: req.headers, limits: { files: 1, fileSize: maxChunkSize * 1000 * 1000 } })
|
|
||||||
|
|
||||||
busboy.on("file", (fieldname, fileStream) => {
|
|
||||||
fileStream.on("limit", () => {
|
|
||||||
limitReached = true
|
|
||||||
fileStream.resume()
|
|
||||||
})
|
|
||||||
|
|
||||||
getFileStatus = handleFile(tmpDir, req.headers, fileStream)
|
|
||||||
})
|
|
||||||
|
|
||||||
busboy.on("close", () => {
|
|
||||||
if (limitReached) {
|
|
||||||
reject(new Error("Chunk is above size limit"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
getFileStatus((fileErr, assembleChunksF) => {
|
|
||||||
if (fileErr) reject(fileErr)
|
|
||||||
else resolve(assembleChunksF)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
req.pipe(busboy)
|
|
||||||
}
|
|
||||||
|
|
||||||
catch (err) {
|
|
||||||
reject(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default uploadFile
|
|
40
packages/server/classes/Limits/index.js
Normal file
40
packages/server/classes/Limits/index.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Config } from "@db_models"
|
||||||
|
|
||||||
|
export default class Limits {
|
||||||
|
static async get(key) {
|
||||||
|
const { value } = await Config.findOne({
|
||||||
|
key: "limits"
|
||||||
|
}).catch(() => {
|
||||||
|
return {
|
||||||
|
value: {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const limits = {
|
||||||
|
maxChunkSizeInMB: 5,
|
||||||
|
maxFileSizeInMB: 8,
|
||||||
|
maxNumberOfFiles: 10,
|
||||||
|
maxPostCharacters: 2000,
|
||||||
|
maxAccountsPerIp: 10,
|
||||||
|
...value,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof key === "string") {
|
||||||
|
return {
|
||||||
|
value: limits[key] ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(key)) {
|
||||||
|
const result = {}
|
||||||
|
|
||||||
|
key.forEach((k) => {
|
||||||
|
result[k] = limits[k] ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return limits
|
||||||
|
}
|
||||||
|
}
|
@ -8,5 +8,6 @@ export default {
|
|||||||
attachments: { type: Array, default: [] },
|
attachments: { type: Array, default: [] },
|
||||||
flags: { type: Array, default: [] },
|
flags: { type: Array, default: [] },
|
||||||
reply_to: { type: String, default: null },
|
reply_to: { type: String, default: null },
|
||||||
|
updated_at: { type: String, default: null },
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
name: "SavedPost",
|
name: "PostSave",
|
||||||
collection: "savedPosts",
|
collection: "post_saves",
|
||||||
schema: {
|
schema: {
|
||||||
post_id: {
|
post_id: {
|
||||||
type: "string",
|
type: "string",
|
@ -15,6 +15,6 @@ export default {
|
|||||||
badges: { type: Array, default: [] },
|
badges: { type: Array, default: [] },
|
||||||
links: { type: Array, default: [] },
|
links: { type: Array, default: [] },
|
||||||
location: { type: String, default: null },
|
location: { type: String, default: null },
|
||||||
birthday: { type: Date, default: null },
|
birthday: { type: Date, default: null, select: false },
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -10,18 +10,16 @@ import chalk from "chalk"
|
|||||||
import Spinnies from "spinnies"
|
import Spinnies from "spinnies"
|
||||||
import chokidar from "chokidar"
|
import chokidar from "chokidar"
|
||||||
import IPCRouter from "linebridge/src/server/classes/IPCRouter"
|
import IPCRouter from "linebridge/src/server/classes/IPCRouter"
|
||||||
import fastify from "fastify"
|
import treeKill from "tree-kill"
|
||||||
import { createProxyMiddleware } from "http-proxy-middleware"
|
|
||||||
|
|
||||||
import { dots as DefaultSpinner } from "spinnies/spinners.json"
|
import { dots as DefaultSpinner } from "spinnies/spinners.json"
|
||||||
import getInternalIp from "./lib/getInternalIp"
|
import getInternalIp from "./lib/getInternalIp"
|
||||||
import comtyAscii from "./ascii"
|
import comtyAscii from "./ascii"
|
||||||
import pkg from "./package.json"
|
import pkg from "./package.json"
|
||||||
|
|
||||||
import cors from "linebridge/src/server/middlewares/cors"
|
|
||||||
|
|
||||||
import { onExit } from "signal-exit"
|
import { onExit } from "signal-exit"
|
||||||
|
|
||||||
|
import Proxy from "./proxy"
|
||||||
|
|
||||||
const bootloaderBin = path.resolve(__dirname, "boot")
|
const bootloaderBin = path.resolve(__dirname, "boot")
|
||||||
const servicesPath = path.resolve(__dirname, "services")
|
const servicesPath = path.resolve(__dirname, "services")
|
||||||
@ -51,7 +49,7 @@ async function scanServices() {
|
|||||||
return finalServices
|
return finalServices
|
||||||
}
|
}
|
||||||
|
|
||||||
let internal_proxy = null
|
let internal_proxy = new Proxy()
|
||||||
let allReady = false
|
let allReady = false
|
||||||
let selectedProcessInstance = null
|
let selectedProcessInstance = null
|
||||||
let internalIp = null
|
let internalIp = null
|
||||||
@ -72,7 +70,7 @@ Observable.observe(serviceRegistry, (changes) => {
|
|||||||
//console.log(`Updated service | ${path} > ${value}`)
|
//console.log(`Updated service | ${path} > ${value}`)
|
||||||
|
|
||||||
//check if all services all ready
|
//check if all services all ready
|
||||||
if (Object.values(serviceRegistry).every((service) => service.ready)) {
|
if (Object.values(serviceRegistry).every((service) => service.initialized)) {
|
||||||
handleAllReady()
|
handleAllReady()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,6 +174,8 @@ async function handleAllReady() {
|
|||||||
console.log(comtyAscii)
|
console.log(comtyAscii)
|
||||||
console.log(`🎉 All services[${services.length}] ready!\n`)
|
console.log(`🎉 All services[${services.length}] ready!\n`)
|
||||||
console.log(`USE: select <service>, reboot, exit`)
|
console.log(`USE: select <service>, reboot, exit`)
|
||||||
|
|
||||||
|
await internal_proxy.listen(9000, "0.0.0.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SERVICE WATCHER FUNCTIONS
|
// SERVICE WATCHER FUNCTIONS
|
||||||
@ -189,6 +189,8 @@ async function handleNewServiceStarting(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleServiceStarted(id) {
|
async function handleServiceStarted(id) {
|
||||||
|
serviceRegistry[id].initialized = true
|
||||||
|
|
||||||
if (serviceRegistry[id].ready === false) {
|
if (serviceRegistry[id].ready === false) {
|
||||||
if (spinnies.pick(id)) {
|
if (spinnies.pick(id)) {
|
||||||
spinnies.succeed(id, { text: `[${id}][${serviceRegistry[id].index}] Ready` })
|
spinnies.succeed(id, { text: `[${id}][${serviceRegistry[id].index}] Ready` })
|
||||||
@ -199,7 +201,7 @@ async function handleServiceStarted(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleServiceExit(id, code, err) {
|
async function handleServiceExit(id, code, err) {
|
||||||
//console.log(`🛑 Service ${id} exited with code ${code}`, err)
|
serviceRegistry[id].initialized = true
|
||||||
|
|
||||||
if (serviceRegistry[id].ready === false) {
|
if (serviceRegistry[id].ready === false) {
|
||||||
if (spinnies.pick(id)) {
|
if (spinnies.pick(id)) {
|
||||||
@ -207,29 +209,14 @@ async function handleServiceExit(id, code, err) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[${id}] Exit with code ${code}`)
|
||||||
|
|
||||||
|
// try to unregister from proxy
|
||||||
|
internal_proxy.unregisterAllFromService(id)
|
||||||
|
|
||||||
serviceRegistry[id].ready = false
|
serviceRegistry[id].ready = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function registerProxy(_path, target, pathRewrite) {
|
|
||||||
if (internal_proxy.proxys.has(_path)) {
|
|
||||||
console.warn(`Proxy already registered [${_path}], skipping...`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🔗 Registering path proxy [${_path}] -> [${target}]`)
|
|
||||||
|
|
||||||
internal_proxy.proxys.add(_path)
|
|
||||||
|
|
||||||
internal_proxy.use(_path, createProxyMiddleware({
|
|
||||||
target: target,
|
|
||||||
changeOrigin: true,
|
|
||||||
pathRewrite: pathRewrite,
|
|
||||||
ws: true,
|
|
||||||
logLevel: "silent",
|
|
||||||
}))
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleIPCData(service_id, msg) {
|
async function handleIPCData(service_id, msg) {
|
||||||
if (msg.type === "log") {
|
if (msg.type === "log") {
|
||||||
@ -243,21 +230,35 @@ async function handleIPCData(service_id, msg) {
|
|||||||
if (msg.type === "router:register") {
|
if (msg.type === "router:register") {
|
||||||
if (msg.data.path_overrides) {
|
if (msg.data.path_overrides) {
|
||||||
for await (let pathOverride of msg.data.path_overrides) {
|
for await (let pathOverride of msg.data.path_overrides) {
|
||||||
await registerProxy(
|
await internal_proxy.register({
|
||||||
`/${pathOverride}`,
|
serviceId: service_id,
|
||||||
`http://${internalIp}:${msg.data.listen.port}/${pathOverride}`,
|
path: `/${pathOverride}`,
|
||||||
{
|
target: `http://${internalIp}:${msg.data.listen.port}/${pathOverride}`,
|
||||||
|
pathRewrite: {
|
||||||
[`^/${pathOverride}`]: "",
|
[`^/${pathOverride}`]: "",
|
||||||
}
|
},
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await registerProxy(
|
await internal_proxy.register({
|
||||||
`/${service_id}`,
|
serviceId: service_id,
|
||||||
`http://${msg.data.listen.ip}:${msg.data.listen.port}`
|
path: `/${service_id}`,
|
||||||
)
|
target: `http://${msg.data.listen.ip}:${msg.data.listen.port}`,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msg.type === "router:ws:register") {
|
||||||
|
await internal_proxy.register({
|
||||||
|
serviceId: service_id,
|
||||||
|
path: `/${msg.data.namespace}`,
|
||||||
|
target: `http://${internalIp}:${msg.data.listen.port}/${msg.data.namespace}`,
|
||||||
|
pathRewrite: {
|
||||||
|
[`^/${msg.data.namespace}`]: "",
|
||||||
|
},
|
||||||
|
ws: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function spawnService({ id, service, cwd }) {
|
function spawnService({ id, service, cwd }) {
|
||||||
@ -276,11 +277,15 @@ function spawnService({ id, service, cwd }) {
|
|||||||
silent: true,
|
silent: true,
|
||||||
cwd: cwd,
|
cwd: cwd,
|
||||||
env: instanceEnv,
|
env: instanceEnv,
|
||||||
|
killSignal: "SIGKILL",
|
||||||
})
|
})
|
||||||
|
|
||||||
instance.reload = () => {
|
instance.reload = () => {
|
||||||
ipcRouter.unregister({ id, instance })
|
ipcRouter.unregister({ id, instance })
|
||||||
|
|
||||||
|
// try to unregister from proxy
|
||||||
|
internal_proxy.unregisterAllFromService(id)
|
||||||
|
|
||||||
instance.kill()
|
instance.kill()
|
||||||
|
|
||||||
instance = spawnService({ id, service, cwd })
|
instance = spawnService({ id, service, cwd })
|
||||||
@ -340,31 +345,6 @@ function createServiceLogTransformer({ id, color = "bgCyan" }) {
|
|||||||
async function main() {
|
async function main() {
|
||||||
internalIp = await getInternalIp()
|
internalIp = await getInternalIp()
|
||||||
|
|
||||||
internal_proxy = fastify()
|
|
||||||
|
|
||||||
internal_proxy.proxys = new Set()
|
|
||||||
|
|
||||||
await internal_proxy.register(require("@fastify/middie"))
|
|
||||||
|
|
||||||
await internal_proxy.use(cors)
|
|
||||||
|
|
||||||
internal_proxy.get("/ping", (request, reply) => {
|
|
||||||
return reply.send({
|
|
||||||
status: "ok"
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
internal_proxy.get("/", (request, reply) => {
|
|
||||||
return reply.send({
|
|
||||||
services: instancePool.map((instance) => {
|
|
||||||
return {
|
|
||||||
id: instance.id,
|
|
||||||
version: instance.version,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
console.clear()
|
console.clear()
|
||||||
console.log(comtyAscii)
|
console.log(comtyAscii)
|
||||||
console.log(`\nRunning ${chalk.bgBlue(`${pkg.name}`)} | ${chalk.bgMagenta(`[v${pkg.version}]`)} | ${internalIp} \n\n\n`)
|
console.log(`\nRunning ${chalk.bgBlue(`${pkg.name}`)} | ${chalk.bgMagenta(`[v${pkg.version}]`)} | ${internalIp} \n\n\n`)
|
||||||
@ -417,6 +397,7 @@ async function main() {
|
|||||||
if (process.env.NODE_ENV === "development") {
|
if (process.env.NODE_ENV === "development") {
|
||||||
const ignored = [
|
const ignored = [
|
||||||
...await getIgnoredFiles(cwd),
|
...await getIgnoredFiles(cwd),
|
||||||
|
"**/.cache/**",
|
||||||
"**/node_modules/**",
|
"**/node_modules/**",
|
||||||
"**/dist/**",
|
"**/dist/**",
|
||||||
"**/build/**",
|
"**/build/**",
|
||||||
@ -438,7 +419,6 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// create repl
|
|
||||||
repl.start({
|
repl.start({
|
||||||
prompt: "> ",
|
prompt: "> ",
|
||||||
useGlobal: true,
|
useGlobal: true,
|
||||||
@ -474,11 +454,6 @@ async function main() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await internal_proxy.listen({
|
|
||||||
host: "0.0.0.0",
|
|
||||||
port: 9000
|
|
||||||
})
|
|
||||||
|
|
||||||
onExit((code, signal) => {
|
onExit((code, signal) => {
|
||||||
console.clear()
|
console.clear()
|
||||||
console.log(`\n🛑 Preparing to exit...`)
|
console.log(`\n🛑 Preparing to exit...`)
|
||||||
@ -493,7 +468,11 @@ async function main() {
|
|||||||
console.log(`Killing ${instance.id} [${instance.instance.pid}]`)
|
console.log(`Killing ${instance.id} [${instance.instance.pid}]`)
|
||||||
|
|
||||||
instance.instance.kill()
|
instance.instance.kill()
|
||||||
|
|
||||||
|
treeKill(instance.instance.pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
treeKill(process.pid)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,14 +24,21 @@
|
|||||||
"clui": "^0.3.6",
|
"clui": "^0.3.6",
|
||||||
"dotenv": "^16.4.4",
|
"dotenv": "^16.4.4",
|
||||||
"fastify": "^4.26.2",
|
"fastify": "^4.26.2",
|
||||||
"http-proxy-middleware": "^2.0.6",
|
"http-proxy": "^1.18.1",
|
||||||
|
"http-proxy-middleware": "^3.0.0-beta.1",
|
||||||
|
"hyper-express": "^6.14.12",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"linebridge": "^0.18.1",
|
"linebridge": "^0.18.1",
|
||||||
"module-alias": "^2.2.3",
|
"module-alias": "^2.2.3",
|
||||||
"p-map": "^4.0.0",
|
"p-map": "^4.0.0",
|
||||||
"p-queue": "^7.3.4",
|
"p-queue": "^7.3.4",
|
||||||
|
"radix3": "^1.1.1",
|
||||||
"signal-exit": "^4.1.0",
|
"signal-exit": "^4.1.0",
|
||||||
"spinnies": "^0.5.1"
|
"spinnies": "^0.5.1",
|
||||||
|
"tree-kill": "^1.2.2",
|
||||||
|
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.41.0",
|
||||||
|
"uws-reverse-proxy": "^3.2.1",
|
||||||
|
"yume-server": "^0.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^5.1.0",
|
"chai": "^5.1.0",
|
||||||
|
188
packages/server/proxy.js
Normal file
188
packages/server/proxy.js
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import http from "node:http"
|
||||||
|
import httpProxy from "http-proxy"
|
||||||
|
import defaults from "linebridge/src/server/defaults"
|
||||||
|
|
||||||
|
import pkg from "./package.json"
|
||||||
|
|
||||||
|
export default class Proxy {
|
||||||
|
constructor() {
|
||||||
|
this.proxys = new Map()
|
||||||
|
this.wsProxys = new Map()
|
||||||
|
|
||||||
|
this.http = http.createServer(this.handleHttpRequest)
|
||||||
|
this.http.on("upgrade", this.handleHttpUpgrade)
|
||||||
|
}
|
||||||
|
|
||||||
|
http = null
|
||||||
|
|
||||||
|
register = ({ serviceId, path, target, pathRewrite, ws } = {}) => {
|
||||||
|
if (!path) {
|
||||||
|
throw new Error("Path is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
throw new Error("Target is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.proxys.has(path)) {
|
||||||
|
console.warn(`Proxy already registered [${path}], skipping...`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxy = httpProxy.createProxyServer({
|
||||||
|
target: target,
|
||||||
|
})
|
||||||
|
|
||||||
|
proxy.on("error", (e) => {
|
||||||
|
console.error(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
const proxyObj = {
|
||||||
|
serviceId: serviceId ?? "default_service",
|
||||||
|
path: path,
|
||||||
|
target: target,
|
||||||
|
pathRewrite: pathRewrite,
|
||||||
|
proxy: proxy,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ws) {
|
||||||
|
console.log(`🔗 Registering websocket proxy [${path}] -> [${target}]`)
|
||||||
|
this.wsProxys.set(path, proxyObj)
|
||||||
|
} else {
|
||||||
|
console.log(`🔗 Registering path proxy [${path}] -> [${target}]`)
|
||||||
|
this.proxys.set(path, proxyObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister = (path) => {
|
||||||
|
if (!this.proxys.has(path)) {
|
||||||
|
console.warn(`Proxy not registered [${path}], skipping...`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔗 Unregistering path proxy [${path}]`)
|
||||||
|
|
||||||
|
this.proxys.get(path).proxy.close()
|
||||||
|
this.proxys.delete(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
unregisterAllFromService = (serviceId) => {
|
||||||
|
this.proxys.forEach((value, key) => {
|
||||||
|
if (value.serviceId === serviceId) {
|
||||||
|
this.unregister(value.path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
listen = async (port = 9000, host = "0.0.0.0", cb) => {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
this.http.listen(port, host, () => {
|
||||||
|
console.log(`🔗 Proxy listening on ${host}:${port}`)
|
||||||
|
|
||||||
|
if (cb) {
|
||||||
|
cb(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(this)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
rewritePath = (rewriteConfig, path) => {
|
||||||
|
let result = path
|
||||||
|
const rules = []
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(rewriteConfig)) {
|
||||||
|
rules.push({
|
||||||
|
regex: new RegExp(key),
|
||||||
|
value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (rule.regex.test(path)) {
|
||||||
|
result = result.replace(rule.regex, rule.value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
setCorsHeaders = (res) => {
|
||||||
|
res.setHeader("Access-Control-Allow-Origin", "*")
|
||||||
|
res.setHeader("Access-Control-Allow-Methods", "GET,HEAD,PUT,PATCH,POST,DELETE")
|
||||||
|
res.setHeader("Access-Control-Allow-Headers", "*")
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHttpRequest = (req, res) => {
|
||||||
|
res = this.setCorsHeaders(res)
|
||||||
|
|
||||||
|
const sanitizedUrl = req.url.split("?")[0]
|
||||||
|
|
||||||
|
// preflight continue with code 204
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
res.statusCode = 204
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sanitizedUrl === "/") {
|
||||||
|
return res.end(`
|
||||||
|
{
|
||||||
|
"name": "${pkg.name}",
|
||||||
|
"version": "${pkg.version}",
|
||||||
|
"lb_version": "${defaults.version}"
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const namespace = `/${sanitizedUrl.split("/")[1]}`
|
||||||
|
const route = this.proxys.get(namespace)
|
||||||
|
|
||||||
|
if (!route) {
|
||||||
|
res.statusCode = 404
|
||||||
|
res.end(`
|
||||||
|
{
|
||||||
|
"error": "404 Not found"
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.pathRewrite) {
|
||||||
|
req.url = this.rewritePath(route.pathRewrite, req.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log(`HTTP REQUEST :`, req.url)
|
||||||
|
|
||||||
|
route.proxy.web(req, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHttpUpgrade = (req, socket, head) => {
|
||||||
|
const namespace = `/${req.url.split("/")[1]}`
|
||||||
|
const route = this.wsProxys.get(namespace)
|
||||||
|
|
||||||
|
if (!route) {
|
||||||
|
// destroy socket
|
||||||
|
socket.destroy()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.pathRewrite) {
|
||||||
|
req.url = this.rewritePath(route.pathRewrite, req.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
//console.log(`HTTP UPGRADING :`, req.url)
|
||||||
|
|
||||||
|
route.proxy.ws(req, socket, head)
|
||||||
|
}
|
||||||
|
|
||||||
|
close = () => {
|
||||||
|
this.http.close()
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,7 @@ export default async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (userConfig && userConfig.values) {
|
if (userConfig && userConfig.values) {
|
||||||
if (userConfig.values.mfa_enabled) {
|
if (userConfig.values["auth:mfa"]) {
|
||||||
let codeVerified = false
|
let codeVerified = false
|
||||||
|
|
||||||
// search if is already a mfa session
|
// search if is already a mfa session
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import path from "path"
|
|
||||||
import createRoutesFromDirectory from "@utils/createRoutesFromDirectory"
|
|
||||||
import getMiddlewares from "@utils/getMiddlewares"
|
|
||||||
|
|
||||||
export default async (router) => {
|
|
||||||
const routesPath = path.resolve(__dirname, "routes")
|
|
||||||
|
|
||||||
const middlewares = await getMiddlewares(["withOptionalAuth"])
|
|
||||||
|
|
||||||
for (const middleware of middlewares) {
|
|
||||||
router.use(middleware)
|
|
||||||
}
|
|
||||||
|
|
||||||
router = createRoutesFromDirectory("routes", routesPath, router)
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: "/stream",
|
|
||||||
router,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import { NotFoundError, InternalServerError } from "@shared-classes/Errors"
|
|
||||||
import mimetypes from "mime-types"
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
|
||||||
const streamPath = req.params[0]
|
|
||||||
|
|
||||||
global.storage.getObject(process.env.S3_BUCKET, streamPath, (err, dataStream) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(err)
|
|
||||||
return new InternalServerError(req, res, "Error while getting file from storage")
|
|
||||||
}
|
|
||||||
|
|
||||||
const extname = mimetypes.lookup(streamPath)
|
|
||||||
|
|
||||||
// send chunked response
|
|
||||||
res.status(200)
|
|
||||||
|
|
||||||
// set headers
|
|
||||||
res.setHeader("Content-Type", extname)
|
|
||||||
res.setHeader("Accept-Ranges", "bytes")
|
|
||||||
|
|
||||||
return dataStream.pipe(res)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import path from "path"
|
|
||||||
import createRoutesFromDirectory from "@utils/createRoutesFromDirectory"
|
|
||||||
import getMiddlewares from "@utils/getMiddlewares"
|
|
||||||
|
|
||||||
export default async (router) => {
|
|
||||||
const routesPath = path.resolve(__dirname, "routes")
|
|
||||||
|
|
||||||
const middlewares = await getMiddlewares(["withOptionalAuth"])
|
|
||||||
|
|
||||||
for (const middleware of middlewares) {
|
|
||||||
router.use(middleware)
|
|
||||||
}
|
|
||||||
|
|
||||||
router = createRoutesFromDirectory("routes", routesPath, router)
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: "/upload",
|
|
||||||
router,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,107 +0,0 @@
|
|||||||
import path from "path"
|
|
||||||
import fs from "fs"
|
|
||||||
|
|
||||||
import * as Errors from "@shared-classes/Errors"
|
|
||||||
import FileUpload from "@shared-classes/FileUpload"
|
|
||||||
import PostProcess from "@services/post-process"
|
|
||||||
|
|
||||||
const cachePath = global.cache.constructor.cachePath
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
|
||||||
// extract authentification header
|
|
||||||
let auth = req.session
|
|
||||||
|
|
||||||
if (!auth) {
|
|
||||||
return new Errors.AuthorizationError(req, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
const providerType = req.headers["provider-type"]
|
|
||||||
|
|
||||||
const userPath = path.join(cachePath, req.session.user_id)
|
|
||||||
|
|
||||||
// 10 GB in bytes
|
|
||||||
const maxFileSize = 10 * 1000 * 1000 * 1000
|
|
||||||
|
|
||||||
// 10MB in bytes
|
|
||||||
const maxChunkSize = 10 * 1000 * 1000
|
|
||||||
|
|
||||||
let build = await FileUpload(req, userPath, maxFileSize, maxChunkSize)
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("err", err)
|
|
||||||
|
|
||||||
new Errors.InternalServerError(req, res, err.message)
|
|
||||||
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (build === false) {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
if (typeof build === "function") {
|
|
||||||
try {
|
|
||||||
build = await build()
|
|
||||||
|
|
||||||
if (!req.headers["no-compression"]) {
|
|
||||||
build = await PostProcess(build)
|
|
||||||
}
|
|
||||||
|
|
||||||
// compose remote path
|
|
||||||
const remotePath = `${req.session.user_id}/${path.basename(build.filepath)}`
|
|
||||||
|
|
||||||
let url = null
|
|
||||||
|
|
||||||
switch (providerType) {
|
|
||||||
case "premium-cdn": {
|
|
||||||
// use backblaze b2
|
|
||||||
await global.b2Storage.authorize()
|
|
||||||
|
|
||||||
const uploadUrl = await global.b2Storage.getUploadUrl({
|
|
||||||
bucketId: process.env.B2_BUCKET_ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await fs.promises.readFile(build.filepath)
|
|
||||||
|
|
||||||
await global.b2Storage.uploadFile({
|
|
||||||
uploadUrl: uploadUrl.data.uploadUrl,
|
|
||||||
uploadAuthToken: uploadUrl.data.authorizationToken,
|
|
||||||
fileName: remotePath,
|
|
||||||
data: data,
|
|
||||||
info: build.metadata
|
|
||||||
})
|
|
||||||
|
|
||||||
url = `https://${process.env.B2_CDN_ENDPOINT}/${process.env.B2_BUCKET}/${remotePath}`
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
// upload to storage
|
|
||||||
await global.storage.fPutObject(process.env.S3_BUCKET, remotePath, build.filepath, build.metadata ?? {
|
|
||||||
"Content-Type": build.mimetype,
|
|
||||||
})
|
|
||||||
|
|
||||||
// compose url
|
|
||||||
url = global.storage.composeRemoteURL(remotePath)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove from cache
|
|
||||||
fs.promises.rm(build.cachePath, { recursive: true, force: true })
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
name: build.filename,
|
|
||||||
id: remotePath,
|
|
||||||
url: url,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
return new Errors.InternalServerError(req, res, error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
success: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,11 +2,13 @@ import { Server } from "linebridge/src/server"
|
|||||||
|
|
||||||
import B2 from "backblaze-b2"
|
import B2 from "backblaze-b2"
|
||||||
|
|
||||||
|
import DbManager from "@shared-classes/DbManager"
|
||||||
import RedisClient from "@shared-classes/RedisClient"
|
import RedisClient from "@shared-classes/RedisClient"
|
||||||
import StorageClient from "@shared-classes/StorageClient"
|
import StorageClient from "@shared-classes/StorageClient"
|
||||||
import CacheService from "@shared-classes/CacheService"
|
import CacheService from "@shared-classes/CacheService"
|
||||||
|
|
||||||
import SharedMiddlewares from "@shared-middlewares"
|
import SharedMiddlewares from "@shared-middlewares"
|
||||||
|
import LimitsClass from "@shared-classes/Limits"
|
||||||
|
|
||||||
class API extends Server {
|
class API extends Server {
|
||||||
static refName = "files"
|
static refName = "files"
|
||||||
@ -14,13 +16,12 @@ class API extends Server {
|
|||||||
static routesPath = `${__dirname}/routes`
|
static routesPath = `${__dirname}/routes`
|
||||||
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3002
|
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3002
|
||||||
|
|
||||||
static maxBodyLength = 1000 * 1000 * 1000
|
|
||||||
|
|
||||||
middlewares = {
|
middlewares = {
|
||||||
...SharedMiddlewares
|
...SharedMiddlewares
|
||||||
}
|
}
|
||||||
|
|
||||||
contexts = {
|
contexts = {
|
||||||
|
db: new DbManager(),
|
||||||
cache: new CacheService(),
|
cache: new CacheService(),
|
||||||
redis: RedisClient(),
|
redis: RedisClient(),
|
||||||
storage: StorageClient(),
|
storage: StorageClient(),
|
||||||
@ -28,12 +29,19 @@ class API extends Server {
|
|||||||
applicationKeyId: process.env.B2_KEY_ID,
|
applicationKeyId: process.env.B2_KEY_ID,
|
||||||
applicationKey: process.env.B2_APP_KEY,
|
applicationKey: process.env.B2_APP_KEY,
|
||||||
}),
|
}),
|
||||||
|
limits: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
async onInitialize() {
|
async onInitialize() {
|
||||||
|
global.storage = this.contexts.storage
|
||||||
|
global.b2Storage = this.contexts.b2Storage
|
||||||
|
|
||||||
|
await this.contexts.db.initialize()
|
||||||
await this.contexts.redis.initialize()
|
await this.contexts.redis.initialize()
|
||||||
await this.contexts.storage.initialize()
|
await this.contexts.storage.initialize()
|
||||||
await this.contexts.b2Storage.authorize()
|
await this.contexts.b2Storage.authorize()
|
||||||
|
|
||||||
|
this.contexts.limits = await LimitsClass.get()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,104 +1,54 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
|
|
||||||
import FileUpload from "@shared-classes/FileUpload"
|
import ChunkFileUpload from "@shared-classes/ChunkFileUpload"
|
||||||
import PostProcess from "@services/post-process"
|
|
||||||
|
import RemoteUpload from "@services/remoteUpload"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
useContext: ["cache", "storage", "b2Storage"],
|
useContext: ["cache", "limits"],
|
||||||
middlewares: [
|
middlewares: [
|
||||||
"withAuthentication",
|
"withAuthentication",
|
||||||
],
|
],
|
||||||
fn: async (req, res) => {
|
fn: async (req, res) => {
|
||||||
const { cache, storage, b2Storage } = this.default.contexts
|
|
||||||
|
|
||||||
const providerType = req.headers["provider-type"]
|
const providerType = req.headers["provider-type"]
|
||||||
|
|
||||||
const userPath = path.join(cache.constructor.cachePath, req.session.user_id)
|
const userPath = path.join(this.default.contexts.cache.constructor.cachePath, req.auth.session.user_id)
|
||||||
|
|
||||||
// 10 GB in bytes
|
const tmpPath = path.resolve(userPath)
|
||||||
const maxFileSize = 10 * 1000 * 1000 * 1000
|
|
||||||
|
|
||||||
// 10MB in bytes
|
let build = await ChunkFileUpload(req, {
|
||||||
const maxChunkSize = 10 * 1000 * 1000
|
tmpDir: tmpPath,
|
||||||
|
maxFileSize: parseInt(this.default.contexts.limits.maxFileSizeInMB) * 1024 * 1024,
|
||||||
|
maxChunkSize: parseInt(this.default.contexts.limits.maxChunkSizeInMB) * 1024 * 1024,
|
||||||
|
}).catch((err) => {
|
||||||
|
throw new OperationError(err.code, err.message)
|
||||||
|
})
|
||||||
|
|
||||||
let build = await FileUpload(req, userPath, maxFileSize, maxChunkSize)
|
if (typeof build === "function") {
|
||||||
.catch((err) => {
|
try {
|
||||||
console.log("err", err)
|
build = await build()
|
||||||
|
|
||||||
throw new OperationError(500, err.message)
|
const result = await RemoteUpload({
|
||||||
})
|
parentDir: req.auth.session.user_id,
|
||||||
|
source: build.filePath,
|
||||||
|
service: providerType,
|
||||||
|
useCompression: req.headers["use-compression"] ?? true,
|
||||||
|
cachePath: tmpPath,
|
||||||
|
})
|
||||||
|
|
||||||
if (build === false) {
|
fs.promises.rm(tmpPath, { recursive: true, force: true })
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
if (typeof build === "function") {
|
|
||||||
try {
|
|
||||||
build = await build()
|
|
||||||
|
|
||||||
if (!req.headers["no-compression"]) {
|
return result
|
||||||
build = await PostProcess(build)
|
} catch (error) {
|
||||||
}
|
fs.promises.rm(tmpPath, { recursive: true, force: true })
|
||||||
|
|
||||||
// compose remote path
|
throw new OperationError(error.code ?? 500, error.message ?? "Failed to upload file")
|
||||||
const remotePath = `${req.session.user_id}/${path.basename(build.filepath)}`
|
|
||||||
|
|
||||||
let url = null
|
|
||||||
|
|
||||||
switch (providerType) {
|
|
||||||
case "premium-cdn": {
|
|
||||||
// use backblaze b2
|
|
||||||
await b2Storage.authorize()
|
|
||||||
|
|
||||||
const uploadUrl = await b2Storage.getUploadUrl({
|
|
||||||
bucketId: process.env.B2_BUCKET_ID,
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await fs.promises.readFile(build.filepath)
|
|
||||||
|
|
||||||
await b2Storage.uploadFile({
|
|
||||||
uploadUrl: uploadUrl.data.uploadUrl,
|
|
||||||
uploadAuthToken: uploadUrl.data.authorizationToken,
|
|
||||||
fileName: remotePath,
|
|
||||||
data: data,
|
|
||||||
info: build.metadata
|
|
||||||
})
|
|
||||||
|
|
||||||
url = `https://${process.env.B2_CDN_ENDPOINT}/${process.env.B2_BUCKET}/${remotePath}`
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
// upload to storage
|
|
||||||
await storage.fPutObject(process.env.S3_BUCKET, remotePath, build.filepath, build.metadata ?? {
|
|
||||||
"Content-Type": build.mimetype,
|
|
||||||
})
|
|
||||||
|
|
||||||
// compose url
|
|
||||||
url = storage.composeRemoteURL(remotePath)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove from cache
|
|
||||||
fs.promises.rm(build.cachePath, { recursive: true, force: true })
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
name: build.filename,
|
|
||||||
id: remotePath,
|
|
||||||
url: url,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
|
|
||||||
throw new OperationError(500, error.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.json({
|
return {
|
||||||
success: true,
|
ok: 1
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
48
packages/server/services/files/routes/upload/file/post.js
Normal file
48
packages/server/services/files/routes/upload/file/post.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import path from "node:path"
|
||||||
|
import fs from "node:fs"
|
||||||
|
|
||||||
|
import RemoteUpload from "@services/remoteUpload"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
useContext: ["cache"],
|
||||||
|
middlewares: [
|
||||||
|
"withAuthentication",
|
||||||
|
],
|
||||||
|
fn: async (req, res) => {
|
||||||
|
const { cache } = this.default.contexts
|
||||||
|
|
||||||
|
const providerType = req.headers["provider-type"] ?? "standard"
|
||||||
|
|
||||||
|
const userPath = path.join(cache.constructor.cachePath, req.auth.session.user_id)
|
||||||
|
|
||||||
|
let localFilepath = null
|
||||||
|
let tmpPath = path.resolve(userPath, `${Date.now()}`)
|
||||||
|
|
||||||
|
await req.multipart(async (field) => {
|
||||||
|
if (!field.file) {
|
||||||
|
throw new OperationError(400, "Missing file")
|
||||||
|
}
|
||||||
|
|
||||||
|
localFilepath = path.join(tmpPath, field.file.name)
|
||||||
|
|
||||||
|
const existTmpDir = await fs.promises.stat(tmpPath).then(() => true).catch(() => false)
|
||||||
|
|
||||||
|
if (!existTmpDir) {
|
||||||
|
await fs.promises.mkdir(tmpPath, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
await field.write(localFilepath)
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await RemoteUpload({
|
||||||
|
parentDir: req.auth.session.user_id,
|
||||||
|
source: localFilepath,
|
||||||
|
service: providerType,
|
||||||
|
useCompression: req.headers["use-compression"] ?? true,
|
||||||
|
})
|
||||||
|
|
||||||
|
fs.promises.rm(tmpPath, { recursive: true, force: true })
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
6
packages/server/services/files/routes/upload/get.js
Normal file
6
packages/server/services/files/routes/upload/get.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
useContext: ["cache", "limits"],
|
||||||
|
fn: async () => {
|
||||||
|
return this.default.contexts.limits
|
||||||
|
}
|
||||||
|
}
|
@ -29,11 +29,14 @@ async function processVideo(
|
|||||||
videoBitrate = 2024,
|
videoBitrate = 2024,
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const result = await videoTranscode(file.filepath, file.cachePath, {
|
const result = await videoTranscode(file.filepath, {
|
||||||
videoCodec,
|
videoCodec,
|
||||||
format,
|
format,
|
||||||
audioBitrate,
|
audioBitrate,
|
||||||
videoBitrate: [videoBitrate, true],
|
videoBitrate: [videoBitrate, true],
|
||||||
|
extraOptions: [
|
||||||
|
"-threads 1"
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
file.filepath = result.filepath
|
file.filepath = result.filepath
|
||||||
|
123
packages/server/services/files/services/remoteUpload/index.js
Normal file
123
packages/server/services/files/services/remoteUpload/index.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import fs from "node:fs"
|
||||||
|
import path from "node:path"
|
||||||
|
import mimeTypes from "mime-types"
|
||||||
|
import getFileHash from "@shared-utils/readFileHash"
|
||||||
|
|
||||||
|
import PostProcess from "../post-process"
|
||||||
|
|
||||||
|
export async function standardUpload({
|
||||||
|
source,
|
||||||
|
remotePath,
|
||||||
|
metadata,
|
||||||
|
}) {
|
||||||
|
// upload to storage
|
||||||
|
await global.storage.fPutObject(process.env.S3_BUCKET, remotePath, source, metadata)
|
||||||
|
|
||||||
|
// compose url
|
||||||
|
const url = storage.composeRemoteURL(remotePath)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: remotePath,
|
||||||
|
url: url,
|
||||||
|
metadata: metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function b2Upload({
|
||||||
|
source,
|
||||||
|
remotePath,
|
||||||
|
metadata,
|
||||||
|
}) {
|
||||||
|
// use backblaze b2
|
||||||
|
await b2Storage.authorize()
|
||||||
|
|
||||||
|
const uploadUrl = await global.b2Storage.getUploadUrl({
|
||||||
|
bucketId: process.env.B2_BUCKET_ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await fs.promises.readFile(source)
|
||||||
|
|
||||||
|
await global.b2Storage.uploadFile({
|
||||||
|
uploadUrl: uploadUrl.data.uploadUrl,
|
||||||
|
uploadAuthToken: uploadUrl.data.authorizationToken,
|
||||||
|
fileName: remotePath,
|
||||||
|
data: data,
|
||||||
|
info: metadata
|
||||||
|
})
|
||||||
|
|
||||||
|
const url = `https://${process.env.B2_CDN_ENDPOINT}/${process.env.B2_BUCKET}/${remotePath}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: remotePath,
|
||||||
|
url: url,
|
||||||
|
metadata: metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async ({
|
||||||
|
source,
|
||||||
|
parentDir,
|
||||||
|
service,
|
||||||
|
useCompression,
|
||||||
|
cachePath,
|
||||||
|
}) => {
|
||||||
|
if (!source) {
|
||||||
|
throw new OperationError(500, "source is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
service = "standard"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parentDir) {
|
||||||
|
parentDir = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useCompression) {
|
||||||
|
try {
|
||||||
|
const processOutput = await PostProcess({ filepath: source, cachePath })
|
||||||
|
|
||||||
|
if (processOutput) {
|
||||||
|
if (processOutput.filepath) {
|
||||||
|
source = processOutput.filepath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
throw new OperationError(500, `Failed to process file`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = mimeTypes.lookup(path.basename(source))
|
||||||
|
const hash = await getFileHash(fs.createReadStream(source))
|
||||||
|
|
||||||
|
const remotePath = path.join(parentDir, hash)
|
||||||
|
|
||||||
|
let result = {}
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
"Content-Type": type,
|
||||||
|
"File-Hash": hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (service) {
|
||||||
|
case "b2":
|
||||||
|
result = await b2Upload({
|
||||||
|
remotePath,
|
||||||
|
source,
|
||||||
|
metadata,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case "standard":
|
||||||
|
result = await standardUpload({
|
||||||
|
remotePath,
|
||||||
|
source,
|
||||||
|
metadata,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new OperationError(500, "Unsupported service")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
@ -10,11 +10,20 @@ const defaultParams = {
|
|||||||
format: "webm",
|
format: "webm",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (input, cachePath, params = defaultParams) => {
|
const maxTasks = 5
|
||||||
|
|
||||||
|
export default (input, params = defaultParams) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const filename = path.basename(input)
|
if (!global.ffmpegTasks) {
|
||||||
const outputFilename = `${filename.split(".")[0]}_ff.${params.format ?? "webm"}`
|
global.ffmpegTasks = []
|
||||||
const outputFilepath = `${cachePath}/${outputFilename}`
|
}
|
||||||
|
|
||||||
|
if (global.ffmpegTasks.length >= maxTasks) {
|
||||||
|
return reject(new Error("Too many transcoding tasks"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputFilename = `${path.basename(input).split(".")[0]}_ff.${params.format ?? "webm"}`
|
||||||
|
const outputFilepath = `${path.dirname(input)}/${outputFilename}`
|
||||||
|
|
||||||
console.debug(`[TRANSCODING] Transcoding ${input} to ${outputFilepath}`)
|
console.debug(`[TRANSCODING] Transcoding ${input} to ${outputFilepath}`)
|
||||||
|
|
||||||
@ -22,8 +31,8 @@ export default (input, cachePath, params = defaultParams) => {
|
|||||||
console.debug(`[TRANSCODING] Finished transcode ${input} to ${outputFilepath}`)
|
console.debug(`[TRANSCODING] Finished transcode ${input} to ${outputFilepath}`)
|
||||||
|
|
||||||
return resolve({
|
return resolve({
|
||||||
filepath: outputFilepath,
|
|
||||||
filename: outputFilename,
|
filename: outputFilename,
|
||||||
|
filepath: outputFilepath,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,22 +51,33 @@ export default (input, cachePath, params = defaultParams) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// chain methods
|
// chain methods
|
||||||
Object.keys(commands).forEach((key) => {
|
for (let key in commands) {
|
||||||
if (exec === null) {
|
if (exec === null) {
|
||||||
exec = ffmpeg(commands[key])
|
exec = ffmpeg(commands[key])
|
||||||
} else {
|
continue
|
||||||
if (typeof exec[key] !== "function") {
|
}
|
||||||
console.warn(`[TRANSCODING] Method ${key} is not a function`)
|
|
||||||
return false
|
if (key === "extraOptions" && Array.isArray(commands[key])) {
|
||||||
|
for (const option of commands[key]) {
|
||||||
|
exec = exec.inputOptions(option)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(commands[key])) {
|
continue
|
||||||
exec = exec[key](...commands[key])
|
|
||||||
} else {
|
|
||||||
exec = exec[key](commands[key])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
if (typeof exec[key] !== "function") {
|
||||||
|
console.warn(`[TRANSCODING] Method ${key} is not a function`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(commands[key])) {
|
||||||
|
exec = exec[key](...commands[key])
|
||||||
|
} else {
|
||||||
|
exec = exec[key](commands[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
exec
|
exec
|
||||||
.on("error", onError)
|
.on("error", onError)
|
||||||
|
@ -1,28 +1,16 @@
|
|||||||
import { Server } from "linebridge/src/server"
|
import { Server } from "linebridge/src/server"
|
||||||
|
|
||||||
import { Config, User } from "@db_models"
|
|
||||||
import DbManager from "@shared-classes/DbManager"
|
import DbManager from "@shared-classes/DbManager"
|
||||||
import StorageClient from "@shared-classes/StorageClient"
|
|
||||||
|
|
||||||
import Token from "@lib/token"
|
import StartupDB from "./startup_db"
|
||||||
|
|
||||||
import SharedMiddlewares from "@shared-middlewares"
|
import SharedMiddlewares from "@shared-middlewares"
|
||||||
|
|
||||||
export default class API extends Server {
|
export default class API extends Server {
|
||||||
static refName = "main"
|
static refName = "main"
|
||||||
static useEngine = "hyper-express"
|
static useEngine = "hyper-express"
|
||||||
|
static routesPath = `${__dirname}/routes`
|
||||||
static listen_port = process.env.HTTP_LISTEN_PORT || 3000
|
static listen_port = process.env.HTTP_LISTEN_PORT || 3000
|
||||||
static requireWSAuth = true
|
|
||||||
|
|
||||||
constructor(params) {
|
|
||||||
super(params)
|
|
||||||
|
|
||||||
global.DEFAULT_POSTING_POLICY = {
|
|
||||||
maxMessageLength: 512,
|
|
||||||
maximumFileSize: 80 * 1024 * 1024,
|
|
||||||
maximunFilesPerRequest: 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
middlewares = {
|
middlewares = {
|
||||||
...require("@middlewares").default,
|
...require("@middlewares").default,
|
||||||
@ -31,102 +19,16 @@ export default class API extends Server {
|
|||||||
|
|
||||||
events = require("./events")
|
events = require("./events")
|
||||||
|
|
||||||
storage = global.storage = StorageClient()
|
contexts = {
|
||||||
DB = new DbManager()
|
db: new DbManager(),
|
||||||
|
}
|
||||||
|
|
||||||
async onInitialize() {
|
async onInitialize() {
|
||||||
await this.DB.initialize()
|
await this.contexts.db.initialize()
|
||||||
await this.storage.initialize()
|
await StartupDB()
|
||||||
|
|
||||||
await this.initializeConfigDB()
|
|
||||||
await this.checkSetup()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeConfigDB = async () => {
|
handleWsAuth = require("@shared-lib/handleWsAuth").default
|
||||||
let serverConfig = await Config.findOne({ key: "server" }).catch(() => {
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!serverConfig) {
|
|
||||||
serverConfig = new Config({
|
|
||||||
key: "server",
|
|
||||||
value: {
|
|
||||||
setup: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
await serverConfig.save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkSetup = async () => {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
let setupOk = (await Config.findOne({ key: "server" })).value?.setup ?? false
|
|
||||||
|
|
||||||
if (!setupOk) {
|
|
||||||
console.log("⚠️ Server setup is not complete, running setup proccess.")
|
|
||||||
|
|
||||||
let setupScript = await import("./setup")
|
|
||||||
|
|
||||||
setupScript = setupScript.default ?? setupScript
|
|
||||||
|
|
||||||
try {
|
|
||||||
for await (let script of setupScript) {
|
|
||||||
await script()
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Server setup complete.")
|
|
||||||
|
|
||||||
await Config.updateOne({ key: "server" }, { value: { setup: true } })
|
|
||||||
|
|
||||||
return resolve()
|
|
||||||
} catch (error) {
|
|
||||||
console.log("❌ Server setup failed.")
|
|
||||||
console.error(error)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolve()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleWsAuth = async (socket, token, err) => {
|
|
||||||
try {
|
|
||||||
const validation = await Token.validate(token)
|
|
||||||
|
|
||||||
if (!validation.valid) {
|
|
||||||
if (validation.error) {
|
|
||||||
return err(`auth:server_error`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err(`auth:token_invalid`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const userData = await User.findById(validation.data.user_id).catch((err) => {
|
|
||||||
console.error(`[${socket.id}] failed to get user data caused by server error`, err)
|
|
||||||
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!userData) {
|
|
||||||
return err(`auth:user_failed`)
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.userData = userData
|
|
||||||
socket.token = token
|
|
||||||
socket.session = validation.data
|
|
||||||
|
|
||||||
return {
|
|
||||||
token: token,
|
|
||||||
username: userData.username,
|
|
||||||
user_id: userData._id,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return err(`auth:authentification_failed`, error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Boot(API)
|
Boot(API)
|
7
packages/server/services/main/routes/limits/get.js
Normal file
7
packages/server/services/main/routes/limits/get.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import LimitsClass from "@shared-classes/Limits"
|
||||||
|
|
||||||
|
export default async (req) => {
|
||||||
|
const key = req.query.key
|
||||||
|
|
||||||
|
return await LimitsClass.get(key)
|
||||||
|
}
|
3
packages/server/services/main/routes/ping/get.js
Normal file
3
packages/server/services/main/routes/ping/get.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default () => {
|
||||||
|
return "pong"
|
||||||
|
}
|
47
packages/server/services/main/startup_db.js
Normal file
47
packages/server/services/main/startup_db.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Config } from "@db_models"
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
let serverConfig = await Config.findOne({ key: "server" }).catch(() => {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!serverConfig) {
|
||||||
|
console.log("Server config DB is not created, creating it...")
|
||||||
|
|
||||||
|
serverConfig = new Config({
|
||||||
|
key: "server",
|
||||||
|
value: {
|
||||||
|
setup: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await serverConfig.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupScriptsCompleted = (serverConfig.value?.setup) ?? false
|
||||||
|
|
||||||
|
if (!setupScriptsCompleted) {
|
||||||
|
console.log("⚠️ Server setup is not complete, running setup proccess.")
|
||||||
|
|
||||||
|
let setupScript = await import("./setup")
|
||||||
|
setupScript = setupScript.default ?? setupScript
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (let script of setupScript) {
|
||||||
|
await script()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ Server setup complete.")
|
||||||
|
|
||||||
|
await Config.updateOne({ key: "server" }, { value: { setup: true } })
|
||||||
|
|
||||||
|
serverConfig = await Config.findOne({ key: "server" })
|
||||||
|
|
||||||
|
return resolve()
|
||||||
|
} catch (error) {
|
||||||
|
console.log("❌ Server setup failed.")
|
||||||
|
console.error(error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import { Server } from "linebridge/src/server"
|
||||||
|
|
||||||
|
import DbManager from "@shared-classes/DbManager"
|
||||||
|
import RedisClient from "@shared-classes/RedisClient"
|
||||||
|
|
||||||
|
import SharedMiddlewares from "@shared-middlewares"
|
||||||
|
|
||||||
|
class API extends Server {
|
||||||
|
static refName = "notifications"
|
||||||
|
static useEngine = "hyper-express"
|
||||||
|
static wsRoutesPath = `${__dirname}/ws_routes`
|
||||||
|
static routesPath = `${__dirname}/routes`
|
||||||
|
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3009
|
||||||
|
|
||||||
|
middlewares = {
|
||||||
|
...SharedMiddlewares
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts = {
|
||||||
|
db: new DbManager(),
|
||||||
|
redis: RedisClient(),
|
||||||
|
}
|
||||||
|
|
||||||
|
async onInitialize() {
|
||||||
|
await this.contexts.db.initialize()
|
||||||
|
await this.contexts.redis.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWsAuth = require("@shared-lib/handleWsAuth").default
|
||||||
|
}
|
||||||
|
|
||||||
|
Boot(API)
|
6
packages/server/services/notifications/package.json
Normal file
6
packages/server/services/notifications/package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "notifications",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
export default () =>{
|
||||||
|
return {
|
||||||
|
hi: "hola xd"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
export default async () => {
|
||||||
|
global.rtengine.io.of("/").emit("new", {
|
||||||
|
hi: "hola xd"
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
hi: "hola xd"
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
export default class Posts {
|
export default class Posts {
|
||||||
static feed = require("./methods/feed").default
|
static timeline = require("./methods/timeline").default
|
||||||
|
static globalTimeline = require("./methods/globalTimeline").default
|
||||||
static data = require("./methods/data").default
|
static data = require("./methods/data").default
|
||||||
static getLiked = require("./methods/getLiked").default
|
static getLiked = require("./methods/getLiked").default
|
||||||
static getSaved = require("./methods/getSaved").default
|
static getSaved = require("./methods/getSaved").default
|
||||||
@ -10,4 +11,7 @@ export default class Posts {
|
|||||||
static toggleLike = require("./methods/toggleLike").default
|
static toggleLike = require("./methods/toggleLike").default
|
||||||
static report = require("./methods/report").default
|
static report = require("./methods/report").default
|
||||||
static flag = require("./methods/flag").default
|
static flag = require("./methods/flag").default
|
||||||
|
static delete = require("./methods/delete").default
|
||||||
|
static update = require("./methods/update").default
|
||||||
|
static replies = require("./methods/replies").default
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ import requiredFields from "@shared-utils/requiredFields"
|
|||||||
import { DateTime } from "luxon"
|
import { DateTime } from "luxon"
|
||||||
|
|
||||||
import { Post } from "@db_models"
|
import { Post } from "@db_models"
|
||||||
|
import fullfill from "./fullfill"
|
||||||
|
|
||||||
export default async (payload = {}) => {
|
export default async (payload = {}) => {
|
||||||
await requiredFields(["user_id"], payload)
|
await requiredFields(["user_id"], payload)
|
||||||
@ -32,9 +33,13 @@ export default async (payload = {}) => {
|
|||||||
|
|
||||||
post = post.toObject()
|
post = post.toObject()
|
||||||
|
|
||||||
// TODO: create background jobs (nsfw dectection)
|
const result = await fullfill({
|
||||||
|
posts: post,
|
||||||
|
for_user_id: user_id
|
||||||
|
})
|
||||||
|
|
||||||
// TODO: Push event to Websocket
|
// TODO: create background jobs (nsfw dectection)
|
||||||
|
global.rtengine.io.of("/").emit(`post.new`, result[0])
|
||||||
|
|
||||||
return post
|
return post
|
||||||
}
|
}
|
@ -1,16 +1,23 @@
|
|||||||
import { Post } from "@db_models"
|
import { Post } from "@db_models"
|
||||||
import fullfillPostsData from "./fullfill"
|
import fullfillPostsData from "./fullfill"
|
||||||
|
|
||||||
|
const maxLimit = 300
|
||||||
|
|
||||||
export default async (payload = {}) => {
|
export default async (payload = {}) => {
|
||||||
let {
|
let {
|
||||||
for_user_id,
|
for_user_id,
|
||||||
post_id,
|
post_id,
|
||||||
query = {},
|
query = {},
|
||||||
skip = 0,
|
trim = 0,
|
||||||
limit = 20,
|
limit = 20,
|
||||||
sort = { created_at: -1 },
|
sort = { created_at: -1 },
|
||||||
} = payload
|
} = payload
|
||||||
|
|
||||||
|
// set a hard limit on the number of posts to retrieve, used for pagination
|
||||||
|
if (limit > maxLimit) {
|
||||||
|
limit = maxLimit
|
||||||
|
}
|
||||||
|
|
||||||
let posts = []
|
let posts = []
|
||||||
|
|
||||||
if (post_id) {
|
if (post_id) {
|
||||||
@ -24,7 +31,7 @@ export default async (payload = {}) => {
|
|||||||
} else {
|
} else {
|
||||||
posts = await Post.find({ ...query })
|
posts = await Post.find({ ...query })
|
||||||
.sort(sort)
|
.sort(sort)
|
||||||
.skip(skip)
|
.skip(trim)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +39,6 @@ export default async (payload = {}) => {
|
|||||||
posts = await fullfillPostsData({
|
posts = await fullfillPostsData({
|
||||||
posts,
|
posts,
|
||||||
for_user_id,
|
for_user_id,
|
||||||
skip,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// if post_id is specified, return only one post
|
// if post_id is specified, return only one post
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
import { Post, PostLike, PostSave } from "@db_models"
|
||||||
|
|
||||||
|
export default async (payload = {}) => {
|
||||||
|
let {
|
||||||
|
post_id
|
||||||
|
} = payload
|
||||||
|
|
||||||
|
if (!post_id) {
|
||||||
|
throw new OperationError(400, "Missing post_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
await Post.deleteOne({
|
||||||
|
_id: post_id,
|
||||||
|
}).catch((err) => {
|
||||||
|
throw new OperationError(500, `An error has occurred: ${err.message}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// search for likes
|
||||||
|
await PostLike.deleteMany({
|
||||||
|
post_id: post_id,
|
||||||
|
}).catch((err) => {
|
||||||
|
throw new OperationError(500, `An error has occurred: ${err.message}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// deleted from saved
|
||||||
|
await PostSave.deleteMany({
|
||||||
|
post_id: post_id,
|
||||||
|
}).catch((err) => {
|
||||||
|
throw new OperationError(500, `An error has occurred: ${err.message}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
global.rtengine.io.of("/").emit(`post.delete`, post_id)
|
||||||
|
global.rtengine.io.of("/").emit(`post.delete.${post_id}`, post_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
deleted: true,
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ export default async (payload = {}) => {
|
|||||||
const {
|
const {
|
||||||
for_user_id,
|
for_user_id,
|
||||||
user_id,
|
user_id,
|
||||||
skip,
|
trim,
|
||||||
limit,
|
limit,
|
||||||
} = payload
|
} = payload
|
||||||
|
|
||||||
@ -14,8 +14,8 @@ export default async (payload = {}) => {
|
|||||||
|
|
||||||
return await GetData({
|
return await GetData({
|
||||||
for_user_id: for_user_id,
|
for_user_id: for_user_id,
|
||||||
skip,
|
trim: trim,
|
||||||
limit,
|
limit: limit,
|
||||||
query: {
|
query: {
|
||||||
user_id: {
|
user_id: {
|
||||||
$in: user_id
|
$in: user_id
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { User, Comment, PostLike, SavedPost } from "@db_models"
|
import { User, PostLike, PostSave, Post } from "@db_models"
|
||||||
|
|
||||||
export default async (payload = {}) => {
|
export default async (payload = {}) => {
|
||||||
let {
|
let {
|
||||||
@ -14,33 +14,26 @@ export default async (payload = {}) => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
let savedPostsIds = []
|
let postsSavesIds = []
|
||||||
|
|
||||||
if (for_user_id) {
|
if (for_user_id) {
|
||||||
const savedPosts = await SavedPost.find({ user_id: for_user_id })
|
const postsSaves = await PostSave.find({ user_id: for_user_id })
|
||||||
.sort({ saved_at: -1 })
|
.sort({ saved_at: -1 })
|
||||||
|
|
||||||
savedPostsIds = savedPosts.map((savedPost) => savedPost.post_id)
|
postsSavesIds = postsSaves.map((postSave) => postSave.post_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
let [usersData, likesData, commentsData] = await Promise.all([
|
let [usersData, likesData, repliesData] = await Promise.all([
|
||||||
User.find({
|
User.find({
|
||||||
_id: {
|
_id: {
|
||||||
$in: posts.map((post) => post.user_id)
|
$in: posts.map((post) => post.user_id)
|
||||||
}
|
}
|
||||||
})
|
}).catch(() => { }),
|
||||||
.select("-email")
|
|
||||||
.select("-birthday"),
|
|
||||||
PostLike.find({
|
PostLike.find({
|
||||||
post_id: {
|
post_id: {
|
||||||
$in: posts.map((post) => post._id)
|
$in: posts.map((post) => post._id)
|
||||||
}
|
}
|
||||||
}).catch(() => []),
|
}).catch(() => []),
|
||||||
Comment.find({
|
|
||||||
parent_id: {
|
|
||||||
$in: posts.map((post) => post._id)
|
|
||||||
}
|
|
||||||
}).catch(() => []),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
// wrap likesData by post_id
|
// wrap likesData by post_id
|
||||||
@ -54,19 +47,10 @@ export default async (payload = {}) => {
|
|||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
// wrap commentsData by post_id
|
|
||||||
commentsData = commentsData.reduce((acc, comment) => {
|
|
||||||
if (!acc[comment.parent_id]) {
|
|
||||||
acc[comment.parent_id] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
acc[comment.parent_id].push(comment)
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
posts = await Promise.all(posts.map(async (post, index) => {
|
posts = await Promise.all(posts.map(async (post, index) => {
|
||||||
post = post.toObject()
|
if (typeof post.toObject === "function") {
|
||||||
|
post = post.toObject()
|
||||||
|
}
|
||||||
|
|
||||||
let user = usersData.find((user) => user._id.toString() === post.user_id.toString())
|
let user = usersData.find((user) => user._id.toString() === post.user_id.toString())
|
||||||
|
|
||||||
@ -77,22 +61,21 @@ export default async (payload = {}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (post.reply_to) {
|
||||||
|
post.reply_to_data = await Post.findById(post.reply_to)
|
||||||
|
}
|
||||||
|
|
||||||
let likes = likesData[post._id.toString()] ?? []
|
let likes = likesData[post._id.toString()] ?? []
|
||||||
|
|
||||||
post.countLikes = likes.length
|
post.countLikes = likes.length
|
||||||
|
|
||||||
let comments = commentsData[post._id.toString()] ?? []
|
|
||||||
|
|
||||||
post.countComments = comments.length
|
|
||||||
|
|
||||||
if (for_user_id) {
|
if (for_user_id) {
|
||||||
post.isLiked = likes.some((like) => like.user_id.toString() === for_user_id)
|
post.isLiked = likes.some((like) => like.user_id.toString() === for_user_id)
|
||||||
post.isSaved = savedPostsIds.includes(post._id.toString())
|
post.isSaved = postsSavesIds.includes(post._id.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...post,
|
...post,
|
||||||
comments: comments.map((comment) => comment._id.toString()),
|
|
||||||
user,
|
user,
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user