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
|
||||
3007 -> ems (External Messaging Service)
|
||||
3008 -> users
|
||||
3009 -> unallocated
|
||||
3009 -> notifications
|
||||
3010 -> unallocated
|
||||
3011 -> unallocated
|
||||
3012 -> unallocated
|
||||
|
@ -1,5 +1,4 @@
|
||||
const path = require("path")
|
||||
const { builtinModules } = require("module")
|
||||
|
||||
const aliases = {
|
||||
"node:buffer": "buffer",
|
||||
@ -19,7 +18,7 @@ const aliases = {
|
||||
hooks: path.join(__dirname, "src/hooks"),
|
||||
classes: path.join(__dirname, "src/classes"),
|
||||
"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 = {}) => {
|
||||
@ -30,11 +29,6 @@ module.exports = (config = {}) => {
|
||||
config.server = {}
|
||||
}
|
||||
|
||||
// config.define = {
|
||||
// "global.Uint8Array": "Uint8Array",
|
||||
// "process.env.NODE_DEBUG": false,
|
||||
// }
|
||||
|
||||
config.resolve.alias = aliases
|
||||
config.server.port = process.env.listenPort ?? 8000
|
||||
config.server.host = "0.0.0.0"
|
||||
@ -56,25 +50,5 @@ module.exports = (config = {}) => {
|
||||
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
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
{
|
||||
"low_performance_mode": false,
|
||||
"transcode_video_browser": false,
|
||||
"forceMobileMode": false,
|
||||
"ui.effects": true,
|
||||
"ui.general_volume": 50,
|
||||
|
@ -82,6 +82,12 @@ export default [
|
||||
useLayout: "minimal",
|
||||
public: true
|
||||
},
|
||||
{
|
||||
path: "/marketplace/*",
|
||||
useLayout: "default",
|
||||
centeredContent: true,
|
||||
extendedContent: true,
|
||||
},
|
||||
// THIS MUST BE THE LAST ROUTE
|
||||
{
|
||||
path: "/",
|
||||
|
@ -7,6 +7,8 @@ import { Icons } from "components/Icons"
|
||||
|
||||
import config from "config"
|
||||
|
||||
import LatencyIndicator from "components/PerformanceIndicators/latency"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const connectionsTooltipStrings = {
|
||||
@ -48,9 +50,7 @@ export default {
|
||||
|
||||
const [serverManifest, setServerManifest] = React.useState(null)
|
||||
const [serverOrigin, setServerOrigin] = React.useState(null)
|
||||
const [serverHealth, setServerHealth] = React.useState(null)
|
||||
const [secureConnection, setSecureConnection] = React.useState(false)
|
||||
const [connectionPing, setConnectionPing] = React.useState({})
|
||||
const [capInfo, setCapInfo] = React.useState(null)
|
||||
|
||||
const setCapacitorInfo = async () => {
|
||||
@ -68,7 +68,7 @@ export default {
|
||||
}
|
||||
|
||||
const checkServerOrigin = async () => {
|
||||
const instance = app.cores.api.instance()
|
||||
const instance = app.cores.api.client()
|
||||
|
||||
if (instance) {
|
||||
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(() => {
|
||||
checkServerVersion()
|
||||
checkServerOrigin()
|
||||
|
||||
measurePing()
|
||||
|
||||
setCapacitorInfo()
|
||||
|
||||
const measureInterval = setInterval(() => {
|
||||
measurePing()
|
||||
}, 3000)
|
||||
|
||||
return () => {
|
||||
clearInterval(measureInterval)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div className="about_app">
|
||||
@ -172,33 +154,13 @@ export default {
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Icons.MdHttp />
|
||||
<antd.Tag
|
||||
color={latencyToColor(connectionPing?.http, "http")}
|
||||
>
|
||||
{connectionPing?.http}ms
|
||||
</antd.Tag>
|
||||
</div>
|
||||
<LatencyIndicator
|
||||
type="http"
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Icons.MdSettingsEthernet />
|
||||
<antd.Tag
|
||||
color={latencyToColor(connectionPing?.ws, "ws")}
|
||||
>
|
||||
{connectionPing?.ws}ms
|
||||
</antd.Tag>
|
||||
</div>
|
||||
<LatencyIndicator
|
||||
type="ws"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -62,8 +62,6 @@ const SessionItem = (props) => {
|
||||
return UAParser(session.client)
|
||||
})
|
||||
|
||||
console.log(session, ua)
|
||||
|
||||
return <div
|
||||
className={classnames(
|
||||
"security_sessions_list_item_wrapper",
|
||||
|
@ -21,8 +21,6 @@ export default () => {
|
||||
return null
|
||||
})
|
||||
|
||||
console.log(response)
|
||||
|
||||
if (response) {
|
||||
setSessions(response)
|
||||
}
|
||||
@ -72,7 +70,6 @@ export default () => {
|
||||
return `${total} Sessions`
|
||||
}}
|
||||
simple
|
||||
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -72,13 +72,16 @@ export default {
|
||||
},
|
||||
{
|
||||
id: "low_performance_mode",
|
||||
storaged: true,
|
||||
group: "general",
|
||||
component: "Switch",
|
||||
icon: "MdSlowMotionVideo",
|
||||
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.",
|
||||
emitEvent: "app.lowPerformanceMode",
|
||||
props: {
|
||||
disabled: true
|
||||
},
|
||||
storaged: true,
|
||||
experimental: true,
|
||||
disabled: true,
|
||||
},
|
||||
@ -183,6 +186,23 @@ export default {
|
||||
storaged: true,
|
||||
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",
|
||||
title: "Fetch max items",
|
||||
|
@ -28,28 +28,28 @@ export default {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "fullName",
|
||||
id: "public_name",
|
||||
group: "account.basicInfo",
|
||||
component: "Input",
|
||||
icon: "Edit3",
|
||||
title: "Name",
|
||||
description: "Change your public name",
|
||||
props: {
|
||||
// set max length
|
||||
"maxLength": 120,
|
||||
"showCount": true,
|
||||
"allowClear": true,
|
||||
"placeholder": "Enter your name. e.g. John Doe",
|
||||
maxLength: 120,
|
||||
showCount: true,
|
||||
allowClear: true,
|
||||
placeholder: "Enter your name. e.g. John Doe",
|
||||
},
|
||||
defaultValue: (ctx) => {
|
||||
return ctx.userData.fullName
|
||||
return ctx.userData.public_name
|
||||
},
|
||||
onUpdate: async (value) => {
|
||||
const result = await UserModel.updateData({
|
||||
fullName: value
|
||||
public_name: value
|
||||
})
|
||||
|
||||
if (result) {
|
||||
app.message.success("Public name updated")
|
||||
return value
|
||||
}
|
||||
},
|
||||
@ -67,22 +67,22 @@ export default {
|
||||
storaged: false,
|
||||
},
|
||||
{
|
||||
"id": "email",
|
||||
"group": "account.basicInfo",
|
||||
"component": "Input",
|
||||
"icon": "Mail",
|
||||
"title": "Email",
|
||||
"description": "Change your email address",
|
||||
"props": {
|
||||
"placeholder": "Enter your email address",
|
||||
"allowClear": true,
|
||||
"showCount": true,
|
||||
"maxLength": 320,
|
||||
id: "email",
|
||||
group: "account.basicInfo",
|
||||
component: "Input",
|
||||
icon: "Mail",
|
||||
title: "Email",
|
||||
description: "Change your email address",
|
||||
props: {
|
||||
placeholder: "Enter your email address",
|
||||
allowClear: true,
|
||||
showCount: true,
|
||||
maxLength: 320,
|
||||
},
|
||||
"defaultValue": (ctx) => {
|
||||
defaultValue: (ctx) => {
|
||||
return ctx.userData.email
|
||||
},
|
||||
"onUpdate": async (value) => {
|
||||
onUpdate: async (value) => {
|
||||
const result = await UserModel.updateData({
|
||||
email: value
|
||||
})
|
||||
@ -91,22 +91,22 @@ export default {
|
||||
return value
|
||||
}
|
||||
},
|
||||
"debounced": true,
|
||||
debounced: true,
|
||||
},
|
||||
{
|
||||
"id": "avatar",
|
||||
"group": "account.profile",
|
||||
"icon": "Image",
|
||||
"title": "Avatar",
|
||||
"description": "Change your avatar (Upload an image or use an URL)",
|
||||
"component": loadable(() => import("../components/urlInput")),
|
||||
id: "avatar",
|
||||
group: "account.profile",
|
||||
icon: "Image",
|
||||
title: "Avatar",
|
||||
description: "Change your avatar (Upload an image or use an URL)",
|
||||
component: loadable(() => import("../components/urlInput")),
|
||||
extraActions: [
|
||||
UploadButton
|
||||
],
|
||||
"defaultValue": (ctx) => {
|
||||
defaultValue: (ctx) => {
|
||||
return ctx.userData.avatar
|
||||
},
|
||||
"onUpdate": async (value) => {
|
||||
onUpdate: async (value) => {
|
||||
const result = await UserModel.updateData({
|
||||
avatar: value
|
||||
})
|
||||
@ -118,19 +118,19 @@ export default {
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "cover",
|
||||
"group": "account.profile",
|
||||
"icon": "Image",
|
||||
"title": "Cover",
|
||||
"description": "Change your profile cover (Upload an image or use an URL)",
|
||||
"component": loadable(() => import("../components/urlInput")),
|
||||
id: "cover",
|
||||
group: "account.profile",
|
||||
icon: "Image",
|
||||
title: "Cover",
|
||||
description: "Change your profile cover (Upload an image or use an URL)",
|
||||
component: loadable(() => import("../components/urlInput")),
|
||||
extraActions: [
|
||||
UploadButton
|
||||
],
|
||||
"defaultValue": (ctx) => {
|
||||
defaultValue: (ctx) => {
|
||||
return ctx.userData.cover
|
||||
},
|
||||
"onUpdate": async (value) => {
|
||||
onUpdate: async (value) => {
|
||||
const result = await UserModel.updateData({
|
||||
cover: value
|
||||
})
|
||||
@ -142,22 +142,22 @@ export default {
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "description",
|
||||
"group": "account.profile",
|
||||
"component": "TextArea",
|
||||
"icon": "Edit3",
|
||||
"title": "Description",
|
||||
"description": "Change your description for your profile",
|
||||
"props": {
|
||||
"placeholder": "Enter here a description for your profile",
|
||||
"maxLength": 320,
|
||||
"showCount": true,
|
||||
"allowClear": true
|
||||
id: "description",
|
||||
group: "account.profile",
|
||||
component: "TextArea",
|
||||
icon: "Edit3",
|
||||
title: "Description",
|
||||
description: "Change your description for your profile",
|
||||
props: {
|
||||
placeholder: "Enter here a description for your profile",
|
||||
maxLength: 320,
|
||||
showCount: true,
|
||||
allowClear: true
|
||||
},
|
||||
"defaultValue": (ctx) => {
|
||||
defaultValue: (ctx) => {
|
||||
return ctx.userData.description
|
||||
},
|
||||
"onUpdate": async (value) => {
|
||||
onUpdate: async (value) => {
|
||||
const result = await UserModel.updateData({
|
||||
description: value
|
||||
})
|
||||
@ -166,8 +166,7 @@ export default {
|
||||
return value
|
||||
}
|
||||
},
|
||||
"debounced": true,
|
||||
storaged: false,
|
||||
debounced: true,
|
||||
},
|
||||
{
|
||||
id: "Links",
|
||||
@ -194,7 +193,6 @@ export default {
|
||||
return ctx.userData.links ?? []
|
||||
},
|
||||
debounced: true,
|
||||
storaged: false,
|
||||
}
|
||||
]
|
||||
}
|
@ -7,28 +7,31 @@ export default {
|
||||
group: "basic",
|
||||
settings: [
|
||||
{
|
||||
"id": "change-password",
|
||||
"group": "security.account",
|
||||
"title": "Change Password",
|
||||
"description": "Change your password",
|
||||
"icon": "Lock",
|
||||
"component": loadable(() => import("../components/changePassword")),
|
||||
id: "change-password",
|
||||
group: "security.account",
|
||||
title: "Change Password",
|
||||
description: "Change your password",
|
||||
icon: "Lock",
|
||||
component: loadable(() => import("../components/changePassword")),
|
||||
},
|
||||
{
|
||||
"id": "two-factor-authentication",
|
||||
"group": "security.account",
|
||||
"title": "Two-Factor Authentication",
|
||||
"description": "Add an extra layer of security to your account",
|
||||
"icon": "MdOutlineSecurity",
|
||||
"component": "Switch",
|
||||
id: "auth:mfa",
|
||||
group: "security.account",
|
||||
title: "2-Factor Authentication",
|
||||
description: "Use your email to validate logins to your account through a numerical code.",
|
||||
icon: "IoMdKeypad",
|
||||
component: "Switch",
|
||||
defaultValue: (ctx) => {
|
||||
return ctx.baseConfig["auth:mfa"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sessions",
|
||||
"group": "security.account",
|
||||
"title": "Sessions",
|
||||
"description": "Manage your active sessions",
|
||||
"icon": "Monitor",
|
||||
"component": loadable(() => import("../components/sessions")),
|
||||
id: "sessions",
|
||||
group: "security.account",
|
||||
title: "Sessions",
|
||||
description: "Manage your active sessions",
|
||||
icon: "Monitor",
|
||||
component: loadable(() => import("../components/sessions")),
|
||||
}
|
||||
]
|
||||
}
|
@ -228,6 +228,12 @@ class OwnTags extends React.Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
if (!this.state.data) {
|
||||
return <antd.Empty
|
||||
description="You don't have any tags yet."
|
||||
/>
|
||||
}
|
||||
|
||||
return <div className="tap-share-own_tags">
|
||||
{
|
||||
this.state.data.length === 0 && <antd.Empty
|
||||
|
@ -28,7 +28,6 @@
|
||||
"id": "Marketplace",
|
||||
"path": "/marketplace",
|
||||
"title": "Marketplace",
|
||||
"icon": "Box",
|
||||
"disabled": true
|
||||
"icon": "Box"
|
||||
}
|
||||
]
|
@ -158,6 +158,9 @@ class ComtyApp extends React.Component {
|
||||
"clearAllOverlays": function () {
|
||||
window.app.DrawerController.closeAll()
|
||||
},
|
||||
"app.clearInternalStorage": function () {
|
||||
app.clearInternalStorage()
|
||||
},
|
||||
}
|
||||
|
||||
static publicMethods = {
|
||||
@ -248,8 +251,11 @@ class ComtyApp extends React.Component {
|
||||
/>)
|
||||
},
|
||||
|
||||
openPostCreator: () => {
|
||||
app.layout.modal.open("post_creator", (props) => <PostCreator {...props} />, {
|
||||
openPostCreator: (params) => {
|
||||
app.layout.modal.open("post_creator", (props) => <PostCreator
|
||||
{...props}
|
||||
{...params}
|
||||
/>, {
|
||||
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()
|
||||
|
||||
socket = app.cores.api.instance().wsInstances.chat
|
||||
socket = app.cores.api.instance().sockets.chat
|
||||
|
||||
roomEvents = {
|
||||
"room:recive:message": (message) => {
|
||||
|
@ -50,21 +50,14 @@ export default React.forwardRef((props, ref) => {
|
||||
>
|
||||
{children}
|
||||
|
||||
<div style={{ clear: "both" }} />
|
||||
<lb style={{ clear: "both" }} />
|
||||
|
||||
<div
|
||||
<lb
|
||||
id="bottom"
|
||||
className="bottom"
|
||||
style={{ display: hasMore ? "block" : "none" }}
|
||||
>
|
||||
{loadingComponent && React.createElement(loadingComponent)}
|
||||
</div>
|
||||
|
||||
{/* <div
|
||||
className="no-result"
|
||||
style={{ display: hasMore ? "none" : "block" }}
|
||||
>
|
||||
{noResultComponent ? React.createElement(noResultComponent) : "No more result"}
|
||||
</div> */}
|
||||
</lb>
|
||||
</div>
|
||||
})
|
@ -200,7 +200,8 @@ export default class Login extends React.Component {
|
||||
}
|
||||
|
||||
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 LikeButton from "./likeButton"
|
||||
import CommentsButton from "./commentsButton"
|
||||
import RepliesButton from "./replyButton"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
@ -32,7 +32,7 @@ const MoreActionsItems = [
|
||||
{
|
||||
key: "onClickRepost",
|
||||
label: <>
|
||||
<Icons.Repeat />
|
||||
<Icons.MdCallSplit />
|
||||
<span>Repost</span>
|
||||
</>,
|
||||
},
|
||||
@ -61,7 +61,7 @@ export default (props) => {
|
||||
const {
|
||||
onClickLike,
|
||||
onClickSave,
|
||||
onClickComments,
|
||||
onClickReply,
|
||||
} = props.actions ?? {}
|
||||
|
||||
const genItems = () => {
|
||||
@ -95,10 +95,10 @@ export default (props) => {
|
||||
onClick={onClickSave}
|
||||
/>
|
||||
</div>
|
||||
<div className="action" id="comments">
|
||||
<CommentsButton
|
||||
count={props.commentsCount}
|
||||
onClick={onClickComments}
|
||||
<div className="action" id="replies">
|
||||
<RepliesButton
|
||||
count={props.repliesCount}
|
||||
onClick={onClickReply}
|
||||
/>
|
||||
</div>
|
||||
<div className="action" id="more">
|
||||
|
@ -6,16 +6,16 @@ import "./index.less"
|
||||
|
||||
export default (props) => {
|
||||
return <div
|
||||
className="comments_button"
|
||||
className="reply_button"
|
||||
>
|
||||
<Button
|
||||
type="ghost"
|
||||
shape="circle"
|
||||
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>
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
.comments_button {
|
||||
.reply_button {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
.comments_count {
|
||||
.replies_count {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
@ -1,13 +1,29 @@
|
||||
import React from "react"
|
||||
import { DateTime } from "luxon"
|
||||
import { Tag } from "antd"
|
||||
import { Tag, Skeleton } from "antd"
|
||||
|
||||
import { Image } from "components"
|
||||
import { Icons } from "components/Icons"
|
||||
import PostLink from "components/PostLink"
|
||||
|
||||
import PostService from "models/post"
|
||||
|
||||
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 goToProfile = () => {
|
||||
@ -17,7 +33,12 @@ export default (props) => {
|
||||
const updateTimeAgo = () => {
|
||||
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)
|
||||
}
|
||||
@ -34,22 +55,41 @@ export default (props) => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div className="post_header" onDoubleClick={props.onDoubleClick}>
|
||||
<div className="user">
|
||||
<div className="avatar">
|
||||
return <div className="post-header" onDoubleClick={props.onDoubleClick}>
|
||||
{
|
||||
!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
|
||||
alt="Avatar"
|
||||
src={props.postData.user?.avatar}
|
||||
/>
|
||||
</div>
|
||||
<div className="info">
|
||||
|
||||
<div className="post-header-user-info">
|
||||
<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.flags?.includes("nsfw") && <Tag
|
||||
color="volcano"
|
||||
@ -59,10 +99,12 @@ export default (props) => {
|
||||
}
|
||||
</h1>
|
||||
|
||||
<span className="timeago">
|
||||
<span className="post-header-user-info-timeago">
|
||||
{timeAgo}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default PostCardHeader
|
@ -1,9 +1,26 @@
|
||||
.post_header {
|
||||
.post-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
|
||||
.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;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@ -17,7 +34,7 @@
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
.post-header-user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
@ -30,7 +47,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
.post-header-user-info {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -51,7 +68,7 @@
|
||||
color: var(--background-color-contrast);
|
||||
}
|
||||
|
||||
.timeago {
|
||||
.post-header-user-info-timeago {
|
||||
font-weight: 400;
|
||||
font-size: 0.7rem;
|
||||
|
||||
|
@ -1,11 +1,8 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
import Plyr from "plyr-react"
|
||||
|
||||
import { CommentsCard } from "components"
|
||||
import { motion } from "framer-motion"
|
||||
import { Icons } from "components/Icons"
|
||||
|
||||
import { processString } from "utils"
|
||||
|
||||
import PostHeader from "./components/header"
|
||||
@ -13,6 +10,7 @@ import PostActions from "./components/actions"
|
||||
import PostAttachments from "./components/attachments"
|
||||
|
||||
import "./index.less"
|
||||
import { Divider } from "antd"
|
||||
|
||||
const messageRegexs = [
|
||||
{
|
||||
@ -43,11 +41,14 @@ const messageRegexs = [
|
||||
|
||||
export default class PostCard extends React.PureComponent {
|
||||
state = {
|
||||
data: this.props.data,
|
||||
|
||||
countLikes: this.props.data.countLikes ?? 0,
|
||||
countComments: this.props.data.countComments ?? 0,
|
||||
countReplies: this.props.data.countComments ?? 0,
|
||||
|
||||
hasLiked: this.props.data.isLiked ?? false,
|
||||
hasSaved: this.props.data.isSaved ?? false,
|
||||
hasReplies: this.props.data.hasReplies ?? false,
|
||||
|
||||
open: this.props.defaultOpened ?? false,
|
||||
|
||||
@ -55,13 +56,28 @@ export default class PostCard extends React.PureComponent {
|
||||
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 () => {
|
||||
if (typeof this.props.events.onClickDelete !== "function") {
|
||||
console.warn("onClickDelete event is not a function")
|
||||
return
|
||||
}
|
||||
|
||||
return await this.props.events.onClickDelete(this.props.data)
|
||||
return await this.props.events.onClickDelete(this.state.data)
|
||||
}
|
||||
|
||||
onClickLike = async () => {
|
||||
@ -70,7 +86,16 @@ export default class PostCard extends React.PureComponent {
|
||||
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 () => {
|
||||
@ -79,7 +104,15 @@ export default class PostCard extends React.PureComponent {
|
||||
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 () => {
|
||||
@ -88,57 +121,26 @@ export default class PostCard extends React.PureComponent {
|
||||
return
|
||||
}
|
||||
|
||||
return await this.props.events.onClickEdit(this.props.data)
|
||||
return await this.props.events.onClickEdit(this.state.data)
|
||||
}
|
||||
|
||||
onDoubleClick = async () => {
|
||||
this.handleOpen()
|
||||
}
|
||||
|
||||
onClickComments = async () => {
|
||||
this.handleOpen()
|
||||
}
|
||||
|
||||
handleOpen = (to) => {
|
||||
if (typeof to === "undefined") {
|
||||
to = !this.state.open
|
||||
onClickReply = async () => {
|
||||
if (typeof this.props.events.onClickReply !== "function") {
|
||||
console.warn("onClickReply event is not a function")
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof this.props.events?.ontoggleOpen === "function") {
|
||||
this.props.events?.ontoggleOpen(to, this.props.data)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
open: to,
|
||||
})
|
||||
|
||||
//app.controls.openPostViewer(this.props.data)
|
||||
return await this.props.events.onClickReply(this.state.data)
|
||||
}
|
||||
|
||||
onLikesUpdate = (data) => {
|
||||
console.log("onLikesUpdate", data)
|
||||
|
||||
if (data.to) {
|
||||
componentDidUpdate = (prevProps) => {
|
||||
if (prevProps.data !== this.props.data) {
|
||||
this.setState({
|
||||
countLikes: this.state.countLikes + 1,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
countLikes: this.state.countLikes - 1,
|
||||
data: this.props.data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
console.error(error)
|
||||
|
||||
@ -153,12 +155,28 @@ export default class PostCard extends React.PureComponent {
|
||||
</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() {
|
||||
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}
|
||||
id={this.props.data._id}
|
||||
id={this.state.data._id}
|
||||
post_id={this.state.data._id}
|
||||
style={this.props.style}
|
||||
user-id={this.props.data.user_id}
|
||||
user-id={this.state.data.user_id}
|
||||
context-menu={"postCard-context"}
|
||||
className={classnames(
|
||||
"post_card",
|
||||
@ -168,8 +186,9 @@ export default class PostCard extends React.PureComponent {
|
||||
)}
|
||||
>
|
||||
<PostHeader
|
||||
postData={this.props.data}
|
||||
postData={this.state.data}
|
||||
onDoubleClick={this.onDoubleClick}
|
||||
disableReplyTag={this.props.disableReplyTag}
|
||||
/>
|
||||
|
||||
<div
|
||||
@ -180,37 +199,43 @@ export default class PostCard extends React.PureComponent {
|
||||
>
|
||||
<div className="message">
|
||||
{
|
||||
processString(messageRegexs)(this.props.data.message ?? "")
|
||||
processString(messageRegexs)(this.state.data.message ?? "")
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
!this.props.disableAttachments && this.props.data.attachments && this.props.data.attachments.length > 0 && <PostAttachments
|
||||
attachments={this.props.data.attachments}
|
||||
flags={this.props.data.flags}
|
||||
!this.props.disableAttachments && this.state.data.attachments && this.state.data.attachments.length > 0 && <PostAttachments
|
||||
attachments={this.state.data.attachments}
|
||||
flags={this.state.data.flags}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<PostActions
|
||||
user_id={this.props.data.user_id}
|
||||
user_id={this.state.data.user_id}
|
||||
|
||||
likesCount={this.state.countLikes}
|
||||
commentsCount={this.state.countComments}
|
||||
repliesCount={this.state.countReplies}
|
||||
|
||||
defaultLiked={this.state.hasLiked}
|
||||
defaultSaved={this.state.hasSaved}
|
||||
|
||||
actions={{
|
||||
onClickLike: this.onClickLike,
|
||||
onClickEdit: this.onClickEdit,
|
||||
onClickDelete: this.onClickDelete,
|
||||
onClickSave: this.onClickSave,
|
||||
onClickComments: this.onClickComments,
|
||||
onClickReply: this.onClickReply,
|
||||
}}
|
||||
/>
|
||||
|
||||
<CommentsCard
|
||||
post_id={this.props.data._id}
|
||||
visible={this.state.open}
|
||||
/>
|
||||
</article>
|
||||
{
|
||||
!this.props.disableHasReplies && this.state.hasReplies && <>
|
||||
<Divider />
|
||||
<h1>View replies</h1>
|
||||
</>
|
||||
}
|
||||
|
||||
</motion.div>
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@
|
||||
margin: auto;
|
||||
|
||||
gap: 15px;
|
||||
padding: 17px 17px 0px 17px;
|
||||
padding: 17px 17px 10px 17px;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
@ -22,8 +22,6 @@
|
||||
|
||||
color: rgba(var(--background-color-contrast));
|
||||
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
|
@ -1,24 +1,21 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
import { DateTime } from "luxon"
|
||||
import humanSize from "@tsmx/human-readable"
|
||||
|
||||
import PostLink from "components/PostLink"
|
||||
import { Icons } from "components/Icons"
|
||||
import clipboardEventFileToFile from "utils/clipboardEventFileToFile"
|
||||
|
||||
import clipboardEventFileToFile from "utils/clipboardEventFileToFile"
|
||||
import PostModel from "models/post"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const DEFAULT_POST_POLICY = {
|
||||
maxMessageLength: 512,
|
||||
acceptedMimeTypes: ["image/gif", "image/png", "image/jpeg", "image/bmp"],
|
||||
maximumFileSize: 10 * 1024 * 1024,
|
||||
acceptedMimeTypes: ["image/gif", "image/png", "image/jpeg", "image/bmp", "video/*"],
|
||||
maximunFilesPerRequest: 10
|
||||
}
|
||||
|
||||
// TODO: Fix close window when post created
|
||||
|
||||
export default class PostCreator extends React.Component {
|
||||
state = {
|
||||
@ -92,15 +89,30 @@ export default class PostCreator extends React.Component {
|
||||
const payload = {
|
||||
message: postMessage,
|
||||
attachments: postAttachments,
|
||||
timestamp: DateTime.local().toISO(),
|
||||
//timestamp: DateTime.local().toISO(),
|
||||
}
|
||||
|
||||
const response = await PostModel.create(payload).catch(error => {
|
||||
console.error(error)
|
||||
antd.message.error(error)
|
||||
let response = null
|
||||
|
||||
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({
|
||||
loading: false
|
||||
@ -116,6 +128,10 @@ export default class PostCreator extends React.Component {
|
||||
if (typeof this.props.close === "function") {
|
||||
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) {
|
||||
case "uploading": {
|
||||
this.toggleUploaderVisibility(false)
|
||||
@ -424,9 +438,37 @@ export default class PostCreator extends React.Component {
|
||||
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
|
||||
this.fetchUploadPolicy()
|
||||
//this.fetchUploadPolicy()
|
||||
|
||||
// add a listener to the window
|
||||
document.addEventListener("paste", this.handlePaste)
|
||||
@ -448,6 +490,10 @@ export default class PostCreator extends React.Component {
|
||||
render() {
|
||||
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
|
||||
className={"postCreator"}
|
||||
ref={this.creatorRef}
|
||||
@ -455,6 +501,37 @@ export default class PostCreator extends React.Component {
|
||||
onDragLeave={this.handleDrag}
|
||||
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="avatar">
|
||||
<img src={app.userData?.avatar} />
|
||||
@ -475,7 +552,7 @@ export default class PostCreator extends React.Component {
|
||||
type="primary"
|
||||
disabled={loading || !this.canSubmit()}
|
||||
onClick={this.submit}
|
||||
icon={loading ? <Icons.LoadingOutlined spin /> : <Icons.Send />}
|
||||
icon={loading ? <Icons.LoadingOutlined spin /> : (editMode ? <Icons.MdEdit /> : <Icons.Send />)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,7 +17,29 @@
|
||||
background-color: var(--background-color-accent);
|
||||
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 {
|
||||
display: inline-flex;
|
||||
@ -58,8 +80,6 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
overflow: hidden;
|
||||
overflow-x: scroll;
|
||||
|
||||
@ -125,6 +145,8 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
|
||||
width: 100%;
|
||||
|
||||
overflow-x: auto;
|
||||
@ -146,6 +168,8 @@
|
||||
height: fit-content;
|
||||
|
||||
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 * as antd from "antd"
|
||||
import { Icons } from "components/Icons"
|
||||
import { AnimatePresence } from "framer-motion"
|
||||
|
||||
import PostCard from "components/PostCard"
|
||||
import PlaylistTimelineEntry from "components/Music/PlaylistTimelineEntry"
|
||||
@ -41,21 +42,21 @@ const Entry = React.memo((props) => {
|
||||
return React.createElement(typeToComponent[data.type ?? "post"] ?? PostCard, {
|
||||
key: data._id,
|
||||
data: data,
|
||||
//disableAttachments: true,
|
||||
disableReplyTag: props.disableReplyTag,
|
||||
events: {
|
||||
onClickLike: props.onLikePost,
|
||||
onClickSave: props.onSavePost,
|
||||
onClickDelete: props.onDeletePost,
|
||||
onClickEdit: props.onEditPost,
|
||||
onClickReply: props.onReplyPost,
|
||||
onDoubleClick: props.onDoubleClick,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const PostList = (props) => {
|
||||
const parentRef = React.useRef()
|
||||
|
||||
const PostList = React.forwardRef((props, ref) => {
|
||||
return <LoadMore
|
||||
ref={parentRef}
|
||||
ref={ref}
|
||||
className="post-list"
|
||||
loadingComponent={LoadingComponent}
|
||||
noResultComponent={NoResultComponent}
|
||||
@ -77,40 +78,20 @@ const PostList = (props) => {
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
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"
|
||||
>
|
||||
<AnimatePresence>
|
||||
{
|
||||
(data) => <Entry
|
||||
key={data._id}
|
||||
data={data}
|
||||
onLikePost={props.onLikePost}
|
||||
onSavePost={props.onSavePost}
|
||||
onDeletePost={props.onDeletePost}
|
||||
onEditPost={props.onEditPost}
|
||||
/>
|
||||
props.list.map((data) => {
|
||||
return <Entry
|
||||
key={data._id}
|
||||
data={data}
|
||||
{...props}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</For> */}
|
||||
</AnimatePresence>
|
||||
</LoadMore>
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
export class PostsListsComponent extends React.Component {
|
||||
state = {
|
||||
@ -238,12 +219,15 @@ export class PostsListsComponent extends React.Component {
|
||||
addPost: this.addPost,
|
||||
removePost: this.removePost,
|
||||
addRandomPost: () => {
|
||||
const randomId = Math.random().toString(36).substring(7)
|
||||
|
||||
this.addPost({
|
||||
_id: Math.random().toString(36).substring(7),
|
||||
message: `Random post ${Math.random().toString(36).substring(7)}`,
|
||||
_id: randomId,
|
||||
message: `Random post ${randomId}`,
|
||||
user: {
|
||||
_id: Math.random().toString(36).substring(7),
|
||||
_id: randomId,
|
||||
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`)
|
||||
}
|
||||
|
||||
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`)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
componentDidUpdate = async (prevProps) => {
|
||||
componentDidUpdate = async (prevProps, prevState) => {
|
||||
if (prevProps.list !== this.props.list) {
|
||||
this.setState({
|
||||
list: this.props.list,
|
||||
@ -398,6 +382,22 @@ export class PostsListsComponent extends React.Component {
|
||||
return result
|
||||
}
|
||||
|
||||
onEditPost = (data) => {
|
||||
app.controls.openPostCreator({
|
||||
edit_post: data._id,
|
||||
})
|
||||
}
|
||||
|
||||
onReplyPost = (data) => {
|
||||
app.controls.openPostCreator({
|
||||
reply_to: data._id,
|
||||
})
|
||||
}
|
||||
|
||||
onDoubleClickPost = (data) => {
|
||||
app.navigation.goToPost(data._id)
|
||||
}
|
||||
|
||||
onDeletePost = async (data) => {
|
||||
antd.Modal.confirm({
|
||||
title: "Are you sure you want to delete this post?",
|
||||
@ -444,13 +444,16 @@ export class PostsListsComponent extends React.Component {
|
||||
}
|
||||
|
||||
const PostListProps = {
|
||||
listRef: this.listRef,
|
||||
list: this.state.list,
|
||||
|
||||
disableReplyTag: this.props.disableReplyTag,
|
||||
|
||||
onLikePost: this.onLikePost,
|
||||
onSavePost: this.onSavePost,
|
||||
onDeletePost: this.onDeletePost,
|
||||
onEditPost: this.onEditPost,
|
||||
onReplyPost: this.onReplyPost,
|
||||
onDoubleClick: this.onDoubleClickPost,
|
||||
|
||||
onLoadMore: this.onLoadMore,
|
||||
hasMore: this.state.hasMore,
|
||||
@ -463,12 +466,14 @@ export class PostsListsComponent extends React.Component {
|
||||
|
||||
if (app.isMobile) {
|
||||
return <PostList
|
||||
ref={this.listRef}
|
||||
{...PostListProps}
|
||||
/>
|
||||
}
|
||||
|
||||
return <div className="post-list_wrapper">
|
||||
<PostList
|
||||
ref={this.listRef}
|
||||
{...PostListProps}
|
||||
/>
|
||||
</div>
|
||||
|
@ -59,7 +59,7 @@ html {
|
||||
position: relative;
|
||||
|
||||
// WARN: Only use if is a performance issue (If is using virtualized list)
|
||||
will-change: transform;
|
||||
//will-change: transform;
|
||||
|
||||
overflow: hidden;
|
||||
overflow-y: overlay;
|
||||
@ -77,10 +77,14 @@ html {
|
||||
//margin: auto;
|
||||
z-index: 150;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
.post_card {
|
||||
border-radius: 0;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
|
||||
background-color: transparent;
|
||||
|
||||
&:first-child {
|
||||
border-radius: 8px;
|
||||
|
||||
@ -95,33 +99,11 @@ html {
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.playlistTimelineEntry {
|
||||
border-radius: 0;
|
||||
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;
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
@ -52,7 +52,7 @@ export default class SyncRoomCard extends React.Component {
|
||||
}
|
||||
|
||||
checkLatency = () => {
|
||||
const instance = app.cores.api.instance().wsInstances.music
|
||||
const instance = app.cores.api.instance().sockets.music
|
||||
|
||||
if (instance) {
|
||||
this.setState({
|
||||
@ -67,7 +67,7 @@ export default class SyncRoomCard extends React.Component {
|
||||
})
|
||||
|
||||
// chat instance
|
||||
const chatInstance = app.cores.api.instance().wsInstances.chat
|
||||
const chatInstance = app.cores.api.instance().sockets.chat
|
||||
|
||||
if (chatInstance) {
|
||||
Object.keys(this.chatEvents).forEach((event) => {
|
||||
@ -92,7 +92,7 @@ export default class SyncRoomCard extends React.Component {
|
||||
}
|
||||
|
||||
// chat instance
|
||||
const chatInstance = app.cores.api.instance().wsInstances.chat
|
||||
const chatInstance = app.cores.api.instance().sockets.chat
|
||||
|
||||
if (chatInstance) {
|
||||
Object.keys(this.chatEvents).forEach((event) => {
|
||||
@ -231,7 +231,7 @@ export default class SyncRoomCard extends React.Component {
|
||||
<div className="latency_display">
|
||||
<span>
|
||||
{
|
||||
app.cores.api.instance().wsInstances.music.latency ?? "..."
|
||||
app.cores.api.instance().sockets.music.latency ?? "..."
|
||||
}ms
|
||||
</span>
|
||||
</div>
|
||||
|
@ -1,10 +1,15 @@
|
||||
import React from "react"
|
||||
import { Button, Upload } from "antd"
|
||||
|
||||
import { Upload, Progress } from "antd"
|
||||
import classnames from "classnames"
|
||||
import { Icons } from "components/Icons"
|
||||
|
||||
import useHacks from "hooks/useHacks"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
export default (props) => {
|
||||
const [uploading, setUploading] = React.useState(false)
|
||||
const [progess, setProgess] = React.useState(null)
|
||||
|
||||
const handleOnStart = (file_uid, file) => {
|
||||
if (typeof props.onStart === "function") {
|
||||
@ -32,35 +37,37 @@ export default (props) => {
|
||||
|
||||
const handleUpload = async (req) => {
|
||||
setUploading(true)
|
||||
setProgess(1)
|
||||
|
||||
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) => {
|
||||
return handleOnProgress(file.uid, progress)
|
||||
}
|
||||
}).catch((err) => {
|
||||
app.notification.new({
|
||||
title: "Could not upload file",
|
||||
description: err
|
||||
}, {
|
||||
type: "error"
|
||||
})
|
||||
setProgess(progress)
|
||||
handleOnProgress(file.uid, progress)
|
||||
},
|
||||
onError: (file, error) => {
|
||||
setProgess(null)
|
||||
handleOnError(file.uid, error)
|
||||
setUploading(false)
|
||||
},
|
||||
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
|
||||
@ -69,25 +76,43 @@ export default (props) => {
|
||||
props.multiple ?? false
|
||||
}
|
||||
accept={
|
||||
props.accept ?? "image/*"
|
||||
props.accept ?? [
|
||||
"image/*",
|
||||
"video/*",
|
||||
"audio/*",
|
||||
]
|
||||
}
|
||||
progress={false}
|
||||
fileList={[]}
|
||||
>
|
||||
<Button
|
||||
icon={props.icon ?? <Icons.Upload
|
||||
style={{
|
||||
margin: 0
|
||||
}}
|
||||
/>}
|
||||
loading={uploading}
|
||||
type={
|
||||
props.type ?? "round"
|
||||
className={classnames(
|
||||
"uploadButton",
|
||||
{
|
||||
["uploading"]: !!progess || uploading
|
||||
}
|
||||
>
|
||||
)}
|
||||
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"
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
</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_text">
|
||||
<h1>
|
||||
{user.fullName || user.username}
|
||||
{user.public_name || user.username}
|
||||
{user.verified && <Icons.verifiedBadge />}
|
||||
</h1>
|
||||
<span>
|
||||
|
@ -256,8 +256,6 @@ html {
|
||||
|
||||
outline: 1px solid var(--border-color);
|
||||
|
||||
filter: drop-shadow(0 0 20px var(--border-color));
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
|
@ -15,7 +15,6 @@ export { default as StepsForm } from "./StepsForm"
|
||||
export { default as SearchButton } from "./SearchButton"
|
||||
export { default as Skeleton } from "./Skeleton"
|
||||
export { default as Navigation } from "./Navigation"
|
||||
export { default as ImageUploader } from "./ImageUploader"
|
||||
export { default as ImageViewer } from "./ImageViewer"
|
||||
export { default as Image } from "./Image"
|
||||
export { default as LoadMore } from "./LoadMore"
|
||||
|
@ -2,8 +2,8 @@ import Core from "evite/src/core"
|
||||
|
||||
import createClient from "comty.js"
|
||||
|
||||
import measurePing from "comty.js/handlers/measurePing"
|
||||
import request from "comty.js/handlers/request"
|
||||
import request from "comty.js/request"
|
||||
import measurePing from "comty.js/helpers/measurePing"
|
||||
import useRequest from "comty.js/hooks/useRequest"
|
||||
import { reconnectWebsockets, disconnectWebsockets } from "comty.js"
|
||||
|
||||
@ -13,11 +13,11 @@ export default class APICore extends Core {
|
||||
static bgColor = "coral"
|
||||
static textColor = "black"
|
||||
|
||||
instance = null
|
||||
client = null
|
||||
|
||||
public = {
|
||||
instance: function () {
|
||||
return this.instance
|
||||
client: function () {
|
||||
return this.client
|
||||
}.bind(this),
|
||||
customRequest: request,
|
||||
listenEvent: this.listenEvent.bind(this),
|
||||
@ -28,82 +28,45 @@ export default class APICore extends Core {
|
||||
disconnectWebsockets: disconnectWebsockets,
|
||||
}
|
||||
|
||||
listenEvent(key, handler, instance) {
|
||||
if (!this.instance.wsInstances[instance ?? "default"]) {
|
||||
listenEvent(key, handler, instance = "default") {
|
||||
if (!this.client.sockets[instance]) {
|
||||
console.error(`[API] Websocket instance ${instance} not found`)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return this.instance.wsInstances[instance ?? "default"].on(key, handler)
|
||||
return this.client.sockets[instance].on(key, handler)
|
||||
}
|
||||
|
||||
unlistenEvent(key, handler, instance) {
|
||||
if (!this.instance.wsInstances[instance ?? "default"]) {
|
||||
unlistenEvent(key, handler, instance = "default") {
|
||||
if (!this.client.sockets[instance]) {
|
||||
console.error(`[API] Websocket instance ${instance} not found`)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return this.instance.wsInstances[instance ?? "default"].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)
|
||||
// })
|
||||
// })
|
||||
return this.client.sockets[instance].off(key, handler)
|
||||
}
|
||||
|
||||
async onInitialize() {
|
||||
this.instance = await createClient({
|
||||
this.client = await createClient({
|
||||
enableWs: true,
|
||||
})
|
||||
|
||||
this.instance.eventBus.on("auth:login_success", () => {
|
||||
this.client.eventBus.on("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")
|
||||
})
|
||||
|
||||
this.instance.eventBus.on("session.invalid", (error) => {
|
||||
this.client.eventBus.on("session.invalid", (error) => {
|
||||
app.eventBus.emit("session.invalid", error)
|
||||
})
|
||||
|
||||
// make a basic request to check if the API is available
|
||||
await this.instance.instances["default"]({
|
||||
await this.client.baseRequest({
|
||||
method: "head",
|
||||
url: "/",
|
||||
}).catch((error) => {
|
||||
@ -115,10 +78,6 @@ export default class APICore extends Core {
|
||||
`)
|
||||
})
|
||||
|
||||
this.console.debug("[API] Attached to", this.instance)
|
||||
|
||||
//this.createPingIntervals()
|
||||
|
||||
return this.instance
|
||||
return this.client
|
||||
}
|
||||
}
|
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 { notification as Notf, Space, Button } from "antd"
|
||||
import { Icons, createIconRender } from "components/Icons"
|
||||
import { Translation } from "react-i18next"
|
||||
import { Haptics } from "@capacitor/haptics"
|
||||
|
||||
const NotfTypeToAudio = {
|
||||
info: "notification",
|
||||
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(
|
||||
class NotificationUI {
|
||||
static async notify(
|
||||
notification,
|
||||
options = {
|
||||
type: "info"
|
||||
@ -142,27 +106,6 @@ export default class NotificationCore extends Core {
|
||||
|
||||
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 EventBus from "evite/src/internals/eventBus"
|
||||
import SessionModel from "models/session"
|
||||
|
||||
class 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
import ChunkedUpload from "./chunkedUpload"
|
||||
|
||||
export default class RemoteStorage extends Core {
|
||||
static namespace = "remoteStorage"
|
||||
@ -190,19 +25,15 @@ export default class RemoteStorage extends Core {
|
||||
onProgress = () => { },
|
||||
onFinish = () => { },
|
||||
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) => {
|
||||
const fn = async () => new Promise((resolve, reject) => {
|
||||
const uploader = new ChunkedUpload({
|
||||
endpoint: `${apiEndpoint}/upload/chunk`,
|
||||
chunkSize: chunkSize,
|
||||
endpoint: `${app.cores.api.client().mainOrigin}/upload/chunk`,
|
||||
// TODO: get chunk size from settings
|
||||
splitChunkSize: 5 * 1024 * 1024, // 5MB in bytes
|
||||
file: file,
|
||||
service: service,
|
||||
})
|
||||
@ -210,6 +41,13 @@ export default class RemoteStorage extends Core {
|
||||
uploader.on("error", ({ message }) => {
|
||||
this.console.error("[Uploader] Error", message)
|
||||
|
||||
app.notification.new({
|
||||
title: "Could not upload file",
|
||||
description: message
|
||||
}, {
|
||||
type: "error"
|
||||
})
|
||||
|
||||
if (typeof onError === "function") {
|
||||
onError(file, message)
|
||||
}
|
||||
@ -219,8 +57,6 @@ export default class RemoteStorage extends Core {
|
||||
})
|
||||
|
||||
uploader.on("progress", ({ percentProgress }) => {
|
||||
//this.console.debug(`[Uploader] Progress: ${percentProgress}%`)
|
||||
|
||||
if (typeof onProgress === "function") {
|
||||
onProgress(file, percentProgress)
|
||||
}
|
||||
@ -229,6 +65,12 @@ export default class RemoteStorage extends Core {
|
||||
uploader.on("finish", (data) => {
|
||||
this.console.debug("[Uploader] Finish", data)
|
||||
|
||||
app.notification.new({
|
||||
title: "File uploaded",
|
||||
}, {
|
||||
type: "success"
|
||||
})
|
||||
|
||||
if (typeof onFinish === "function") {
|
||||
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() {
|
||||
this.musicWs = this.ctx.CORES.api.instance.wsInstances.music
|
||||
this.musicWs = this.ctx.CORES.api.instance.sockets.music
|
||||
|
||||
Object.keys(this.hubEvents).forEach((eventName) => {
|
||||
this.musicWs.on(eventName, this.hubEvents[eventName])
|
||||
@ -371,6 +371,7 @@ class MusicSyncSubCore {
|
||||
}
|
||||
|
||||
export default class SyncCore extends Core {
|
||||
static disabled = true
|
||||
static namespace = "sync"
|
||||
static dependencies = ["api", "player"]
|
||||
|
||||
|
@ -6,7 +6,7 @@ export default class WidgetsCore extends Core {
|
||||
static storeKey = "widgets"
|
||||
|
||||
static get apiInstance() {
|
||||
return app.cores.api.instance().instances.marketplace
|
||||
return app.cores.api.client().baseRequest
|
||||
}
|
||||
|
||||
public = {
|
||||
@ -21,7 +21,7 @@ export default class WidgetsCore extends Core {
|
||||
|
||||
async onInitialize() {
|
||||
try {
|
||||
await WidgetsCore.apiInstance()
|
||||
//await WidgetsCore.apiInstance()
|
||||
|
||||
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) => {
|
||||
if (this.contentRef.current && !this.contentRef.current.contains(e.target)) {
|
||||
if (this.props.confirmOnOutsideClick) {
|
||||
return AntdModal.confirm({
|
||||
title: this.props.confirmOnClickTitle ?? "Are you sure?",
|
||||
content: this.props.confirmOnClickContent ?? "Are you sure you want to close this window?",
|
||||
onOk: () => {
|
||||
this.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return this.close()
|
||||
if (this.props.confirmOnOutsideClick) {
|
||||
return AntdModal.confirm({
|
||||
title: this.props.confirmOnClickTitle ?? "Are you sure?",
|
||||
content: this.props.confirmOnClickContent ?? "Are you sure you want to close this window?",
|
||||
onOk: () => {
|
||||
this.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return this.close()
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -82,14 +80,15 @@ class Modal extends React.Component {
|
||||
["framed"]: this.props.framed,
|
||||
}
|
||||
)}
|
||||
onTouchEnd={this.handleClickOutside}
|
||||
onMouseDown={this.handleClickOutside}
|
||||
>
|
||||
<div
|
||||
id="mask_trigger"
|
||||
onTouchEnd={this.handleClickOutside}
|
||||
onMouseDown={this.handleClickOutside}
|
||||
/>
|
||||
<div
|
||||
className="app_modal_content"
|
||||
ref={this.contentRef}
|
||||
onTouchEnd={this.handleClickOutside}
|
||||
onMouseDown={this.handleClickOutside}
|
||||
style={this.props.frameContentStyle}
|
||||
>
|
||||
{
|
||||
|
@ -18,6 +18,17 @@
|
||||
|
||||
transition: all 150ms ease-in-out;
|
||||
|
||||
#mask_trigger {
|
||||
position: fixed;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
&.framed {
|
||||
.app_modal_content {
|
||||
display: flex;
|
||||
|
@ -2,9 +2,10 @@ import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
import { Translation } from "react-i18next"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
|
||||
import { Icons } from "components/Icons"
|
||||
import { Skeleton, FollowButton, UserCard } from "components"
|
||||
import { FollowButton, UserCard } from "components"
|
||||
import { SessionModel, UserModel, FollowsModel } from "models"
|
||||
|
||||
import DetailsTab from "./tabs/details"
|
||||
@ -21,36 +22,6 @@ const TabsComponent = {
|
||||
"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 {
|
||||
state = {
|
||||
requestedUser: null,
|
||||
@ -66,16 +37,8 @@ export default class Account extends React.Component {
|
||||
isNotExistent: false,
|
||||
}
|
||||
|
||||
profileRef = React.createRef()
|
||||
|
||||
contentRef = React.createRef()
|
||||
|
||||
coverComponent = React.createRef()
|
||||
|
||||
leftPanelRef = React.createRef()
|
||||
|
||||
actionsRef = React.createRef()
|
||||
|
||||
componentDidMount = async () => {
|
||||
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 () => {
|
||||
const result = await FollowsModel.toggleFollow({
|
||||
@ -165,8 +121,6 @@ export default class Account extends React.Component {
|
||||
return
|
||||
}
|
||||
|
||||
this.onPostListTopVisibility(true)
|
||||
|
||||
key = key.toLowerCase()
|
||||
|
||||
if (this.state.tabActiveKey === key) {
|
||||
@ -195,11 +149,10 @@ export default class Account extends React.Component {
|
||||
}
|
||||
|
||||
return <div
|
||||
ref={this.profileRef}
|
||||
className={classnames(
|
||||
"accountProfile",
|
||||
{
|
||||
["noCover"]: !user.cover,
|
||||
["withCover"]: user.cover,
|
||||
}
|
||||
)}
|
||||
id="profile"
|
||||
@ -209,7 +162,6 @@ export default class Account extends React.Component {
|
||||
className={classnames("cover", {
|
||||
["expanded"]: this.state.coverExpanded
|
||||
})}
|
||||
ref={this.coverComponent}
|
||||
style={{ backgroundImage: `url("${user.cover}")` }}
|
||||
onClick={() => this.toggleCoverExpanded()}
|
||||
id="profile-cover"
|
||||
@ -217,18 +169,12 @@ export default class Account extends React.Component {
|
||||
}
|
||||
|
||||
<div className="panels">
|
||||
<div
|
||||
className="leftPanel"
|
||||
ref={this.leftPanelRef}
|
||||
>
|
||||
<div className="leftPanel">
|
||||
<UserCard
|
||||
user={user}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="actions"
|
||||
ref={this.actionsRef}
|
||||
>
|
||||
<div className="actions">
|
||||
<FollowButton
|
||||
count={this.state.followersCount}
|
||||
onClick={this.onClickFollow}
|
||||
@ -239,17 +185,33 @@ export default class Account extends React.Component {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="content"
|
||||
className="centerPanel"
|
||||
ref={this.contentRef}
|
||||
>
|
||||
<TabRender
|
||||
renderKey={this.state.tabActiveKey}
|
||||
state={this.state}
|
||||
onTopVisibility={this.onPostListTopVisibility}
|
||||
/>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: 0.15,
|
||||
}}
|
||||
key={this.state.tabActiveKey}
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{
|
||||
React.createElement(TabsComponent[this.state.tabActiveKey], {
|
||||
onTopVisibility: this.onPostListTopVisibility,
|
||||
state: this.state
|
||||
})
|
||||
}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<div className="tabMenuWrapper">
|
||||
<div className="rightPanel">
|
||||
<antd.Menu
|
||||
className="tabMenu"
|
||||
mode={app.isMobile ? "horizontal" : "vertical"}
|
||||
|
@ -1,8 +1,12 @@
|
||||
@import "theme/vars.less";
|
||||
|
||||
@borderRadius: 12px;
|
||||
@stickyCardTop: 20px;
|
||||
@withCoverPanelElevation: 100px;
|
||||
|
||||
.accountProfile {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
@ -10,17 +14,20 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.noCover {
|
||||
&.withCover {
|
||||
.panels {
|
||||
padding-top: 0;
|
||||
|
||||
.leftPanel {
|
||||
transform: translate(0, 0) !important;
|
||||
}
|
||||
}
|
||||
position: sticky;
|
||||
|
||||
.userCard {
|
||||
filter: none;
|
||||
top: calc(@withCoverPanelElevation + @stickyCardTop);
|
||||
left: 0;
|
||||
|
||||
transform: translate(0, -@withCoverPanelElevation);
|
||||
|
||||
.userCard {
|
||||
filter: drop-shadow(0 0 20px var(--border-color));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,14 +88,14 @@
|
||||
|
||||
gap: 20px;
|
||||
|
||||
padding-top: 20px;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.leftPanel {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
top: @stickyCardTop;
|
||||
|
||||
height: fit-content;
|
||||
|
||||
z-index: 55;
|
||||
|
||||
@ -99,14 +106,6 @@
|
||||
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
transform: translate(0, -100px);
|
||||
|
||||
.userCard {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 55;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -131,25 +130,51 @@
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.centerPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.fade-opacity-active {
|
||||
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;
|
||||
top: 0;
|
||||
top: @stickyCardTop;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
padding: 15px;
|
||||
|
||||
height: fit-content;
|
||||
width: fit-content;
|
||||
@ -158,6 +183,19 @@
|
||||
|
||||
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 {
|
||||
background-color: var(--background-color-primary) !important;
|
||||
}
|
||||
@ -168,14 +206,12 @@
|
||||
background-color: var(--background-color-accent);
|
||||
padding: 20px;
|
||||
border-radius: @borderRadius;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 1720px) {
|
||||
.panels {
|
||||
.content {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.leftPanel {
|
||||
.userCard {
|
||||
width: 300px;
|
||||
@ -188,12 +224,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tabMenuWrapper {
|
||||
.rightPanel {
|
||||
width: fit-content;
|
||||
min-width: 0;
|
||||
min-width: 60px;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 10px !important;
|
||||
|
||||
|
@ -98,7 +98,7 @@ export default (props) => {
|
||||
|
||||
<div className="field_value">
|
||||
<p>
|
||||
{props.state.followers.length}
|
||||
{props.state.followersCount}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -117,7 +117,7 @@ export default (props) => {
|
||||
<div className="field_value">
|
||||
<p>
|
||||
{
|
||||
getJoinLabel(Number(props.state.user.createdAt))
|
||||
getJoinLabel(Number(props.state.user.created_at ?? props.state.user.createdAt))
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@ export default (props) => {
|
||||
return <antd.Result
|
||||
status="warning"
|
||||
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 Post from "models/post"
|
||||
import Feed from "models/feed"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
export default class ExplorePosts extends React.Component {
|
||||
render() {
|
||||
return <PostsList
|
||||
loadFromModel={Post.getExplorePosts}
|
||||
loadFromModel={Feed.getGlobalTimelineFeed}
|
||||
watchTimeline={[
|
||||
"post.new",
|
||||
"post.delete",
|
||||
"feed.new",
|
||||
"feed.delete",
|
||||
]}
|
||||
realtime
|
||||
/>
|
||||
|
@ -16,8 +16,9 @@ const emptyListRender = () => {
|
||||
export class SavedPosts extends React.Component {
|
||||
render() {
|
||||
return <PostsList
|
||||
emptyListRender={emptyListRender}
|
||||
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 * as antd from "antd"
|
||||
|
||||
import Post from "models/post"
|
||||
import { PostCard, CommentsCard } from "components"
|
||||
import PostCard from "components/PostCard"
|
||||
import PostsList from "components/PostsList"
|
||||
|
||||
import PostService from "models/post"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
export default (props) => {
|
||||
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 () => {
|
||||
setData(null)
|
||||
|
||||
const data = await Post.getPost({ post_id }).catch(() => {
|
||||
antd.message.error("Failed to get post")
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
if (data) {
|
||||
setData(data)
|
||||
}
|
||||
if (error) {
|
||||
return <antd.Result
|
||||
status="warning"
|
||||
title="Failed to retrieve post"
|
||||
subTitle={error.message}
|
||||
/>
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
loadData()
|
||||
}, [])
|
||||
|
||||
if (!data) {
|
||||
if (loading) {
|
||||
return <antd.Skeleton active />
|
||||
}
|
||||
|
||||
return <div className="postPage">
|
||||
<div className="postWrapper">
|
||||
<PostCard data={data} fullmode />
|
||||
return <div className="post-page">
|
||||
<div className="post-page-original">
|
||||
<h1>Post</h1>
|
||||
|
||||
<PostCard
|
||||
data={result}
|
||||
/>
|
||||
</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>
|
||||
}
|
@ -1,30 +1,8 @@
|
||||
.postPage {
|
||||
.post-page {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
.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;
|
||||
}
|
||||
gap: 20px;
|
||||
}
|
@ -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
|
||||
await this.setState({
|
||||
value: updateValue
|
||||
|
@ -16,50 +16,49 @@ import SettingItemComponent from "../SettingItemComponent"
|
||||
export default class SettingTab extends React.Component {
|
||||
state = {
|
||||
loading: true,
|
||||
processedCtx: {}
|
||||
tab: null,
|
||||
ctx: {},
|
||||
}
|
||||
|
||||
tab = composedTabs[this.props.activeKey]
|
||||
loadTab = async () => {
|
||||
await this.setState({
|
||||
loading: true,
|
||||
processedCtx: {},
|
||||
})
|
||||
|
||||
processCtx = async () => {
|
||||
if (typeof this.tab.ctxData === "function") {
|
||||
this.setState({ loading: true })
|
||||
const tab = composedTabs[this.props.activeKey]
|
||||
|
||||
const resultCtx = await this.tab.ctxData()
|
||||
let ctx = {}
|
||||
|
||||
console.log(resultCtx)
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
processedCtx: resultCtx
|
||||
})
|
||||
if (typeof tab.ctxData === "function") {
|
||||
ctx = await tab.ctxData()
|
||||
}
|
||||
|
||||
await this.setState({
|
||||
tab: tab,
|
||||
loading: false,
|
||||
ctx: {
|
||||
baseConfig: this.props.baseConfig,
|
||||
...ctx
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// check if props.activeKey change
|
||||
componentDidUpdate = async (prevProps) => {
|
||||
if (prevProps.activeKey !== this.props.activeKey) {
|
||||
this.tab = composedTabs[this.props.activeKey]
|
||||
|
||||
this.setState({
|
||||
loading: !!this.tab.ctxData,
|
||||
processedCtx: {}
|
||||
})
|
||||
|
||||
await this.processCtx()
|
||||
await this.loadTab()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount = async () => {
|
||||
this.setState({
|
||||
loading: !!this.tab.ctxData,
|
||||
})
|
||||
await this.loadTab()
|
||||
}
|
||||
|
||||
await this.processCtx()
|
||||
|
||||
this.setState({
|
||||
loading: false
|
||||
})
|
||||
handleSettingUpdate = async (key, value) => {
|
||||
if (typeof this.props.onUpdate === "function") {
|
||||
await this.props.onUpdate(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -67,14 +66,16 @@ export default class SettingTab extends React.Component {
|
||||
return <antd.Skeleton active />
|
||||
}
|
||||
|
||||
if (this.tab.render) {
|
||||
return React.createElement(this.tab.render, {
|
||||
ctx: this.state.processedCtx
|
||||
const { ctx, tab } = this.state
|
||||
|
||||
if (tab.render) {
|
||||
return React.createElement(tab.render, {
|
||||
ctx: ctx,
|
||||
})
|
||||
}
|
||||
|
||||
if (this.props.withGroups) {
|
||||
const group = composeGroupsFromSettingsTab(this.tab.settings)
|
||||
const group = composeGroupsFromSettingsTab(tab.settings)
|
||||
|
||||
return <>
|
||||
{
|
||||
@ -98,9 +99,11 @@ export default class SettingTab extends React.Component {
|
||||
|
||||
<div className="settings_list">
|
||||
{
|
||||
settings.map((setting) => <SettingItemComponent
|
||||
settings.map((setting, index) => <SettingItemComponent
|
||||
key={index}
|
||||
setting={setting}
|
||||
ctx={this.state.processedCtx}
|
||||
ctx={ctx}
|
||||
onUpdate={(value) => this.handleSettingUpdate(setting.id, value)}
|
||||
/>)
|
||||
}
|
||||
</div>
|
||||
@ -109,8 +112,8 @@ export default class SettingTab extends React.Component {
|
||||
}
|
||||
|
||||
{
|
||||
this.tab.footer && React.createElement(this.tab.footer, {
|
||||
ctx: this.state.processedCtx
|
||||
tab.footer && React.createElement(tab.footer, {
|
||||
ctx: this.state.ctx
|
||||
})
|
||||
}
|
||||
</>
|
||||
@ -118,18 +121,22 @@ export default class SettingTab extends React.Component {
|
||||
|
||||
return <>
|
||||
{
|
||||
this.tab.settings.map((setting, index) => {
|
||||
tab.settings.map((setting, index) => {
|
||||
return <SettingItemComponent
|
||||
key={index}
|
||||
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, {
|
||||
ctx: this.state.processedCtx
|
||||
tab.footer && React.createElement(tab.footer, {
|
||||
ctx: this.state.ctx
|
||||
})
|
||||
}
|
||||
</>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from "react"
|
||||
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 { Translation } from "react-i18next"
|
||||
import config from "config"
|
||||
|
||||
import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
|
||||
import useUserRemoteConfig from "hooks/useUserRemoteConfig"
|
||||
|
||||
import {
|
||||
composedSettingsByGroups as settings
|
||||
@ -88,6 +88,7 @@ const generateMenuItems = () => {
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const [config, setConfig, loading] = useUserRemoteConfig()
|
||||
const [activeKey, setActiveKey] = useUrlQueryActiveKey({
|
||||
defaultKey: "general",
|
||||
queryKey: "tab"
|
||||
@ -113,11 +114,14 @@ export default () => {
|
||||
return items
|
||||
}, [])
|
||||
|
||||
return <div
|
||||
className={classnames(
|
||||
"settings_wrapper",
|
||||
)}
|
||||
>
|
||||
function handleOnUpdate(key, value) {
|
||||
setConfig({
|
||||
...config,
|
||||
[key]: value
|
||||
})
|
||||
}
|
||||
|
||||
return <div className="settings_wrapper">
|
||||
<div className="settings_menu">
|
||||
<antd.Menu
|
||||
mode="vertical"
|
||||
@ -128,10 +132,17 @@ export default () => {
|
||||
</div>
|
||||
|
||||
<div className="settings_content">
|
||||
<SettingTab
|
||||
activeKey={activeKey}
|
||||
withGroups
|
||||
/>
|
||||
{
|
||||
loading && <antd.Skeleton active />
|
||||
}
|
||||
{
|
||||
!loading && <SettingTab
|
||||
baseConfig={config}
|
||||
onUpdate={handleOnUpdate}
|
||||
activeKey={activeKey}
|
||||
withGroups
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -95,6 +95,11 @@
|
||||
|
||||
padding: 0 20px;
|
||||
|
||||
.uploadButton{
|
||||
background-color: var(--background-color-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.setting_item_header {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
@ -299,23 +299,100 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ant-notification-notice {
|
||||
background-color: var(--background-color-primary) !important;
|
||||
.ant-notification {
|
||||
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-description {
|
||||
color: var(--text-color) !important;
|
||||
.ant-notification-notice {
|
||||
display: flex;
|
||||
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 {
|
||||
svg {
|
||||
color: var(--ant-error-color) !important;
|
||||
@ -518,30 +596,6 @@
|
||||
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-button-item-wrapper {
|
||||
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: [] },
|
||||
flags: { type: Array, default: [] },
|
||||
reply_to: { type: String, default: null },
|
||||
updated_at: { type: String, default: null },
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
export default {
|
||||
name: "SavedPost",
|
||||
collection: "savedPosts",
|
||||
name: "PostSave",
|
||||
collection: "post_saves",
|
||||
schema: {
|
||||
post_id: {
|
||||
type: "string",
|
@ -15,6 +15,6 @@ export default {
|
||||
badges: { type: Array, default: [] },
|
||||
links: { type: Array, default: [] },
|
||||
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 chokidar from "chokidar"
|
||||
import IPCRouter from "linebridge/src/server/classes/IPCRouter"
|
||||
import fastify from "fastify"
|
||||
import { createProxyMiddleware } from "http-proxy-middleware"
|
||||
import treeKill from "tree-kill"
|
||||
|
||||
import { dots as DefaultSpinner } from "spinnies/spinners.json"
|
||||
import getInternalIp from "./lib/getInternalIp"
|
||||
import comtyAscii from "./ascii"
|
||||
import pkg from "./package.json"
|
||||
|
||||
import cors from "linebridge/src/server/middlewares/cors"
|
||||
|
||||
import { onExit } from "signal-exit"
|
||||
|
||||
import Proxy from "./proxy"
|
||||
|
||||
const bootloaderBin = path.resolve(__dirname, "boot")
|
||||
const servicesPath = path.resolve(__dirname, "services")
|
||||
@ -51,7 +49,7 @@ async function scanServices() {
|
||||
return finalServices
|
||||
}
|
||||
|
||||
let internal_proxy = null
|
||||
let internal_proxy = new Proxy()
|
||||
let allReady = false
|
||||
let selectedProcessInstance = null
|
||||
let internalIp = null
|
||||
@ -72,7 +70,7 @@ Observable.observe(serviceRegistry, (changes) => {
|
||||
//console.log(`Updated service | ${path} > ${value}`)
|
||||
|
||||
//check if all services all ready
|
||||
if (Object.values(serviceRegistry).every((service) => service.ready)) {
|
||||
if (Object.values(serviceRegistry).every((service) => service.initialized)) {
|
||||
handleAllReady()
|
||||
}
|
||||
|
||||
@ -176,6 +174,8 @@ async function handleAllReady() {
|
||||
console.log(comtyAscii)
|
||||
console.log(`🎉 All services[${services.length}] ready!\n`)
|
||||
console.log(`USE: select <service>, reboot, exit`)
|
||||
|
||||
await internal_proxy.listen(9000, "0.0.0.0")
|
||||
}
|
||||
|
||||
// SERVICE WATCHER FUNCTIONS
|
||||
@ -189,6 +189,8 @@ async function handleNewServiceStarting(id) {
|
||||
}
|
||||
|
||||
async function handleServiceStarted(id) {
|
||||
serviceRegistry[id].initialized = true
|
||||
|
||||
if (serviceRegistry[id].ready === false) {
|
||||
if (spinnies.pick(id)) {
|
||||
spinnies.succeed(id, { text: `[${id}][${serviceRegistry[id].index}] Ready` })
|
||||
@ -199,7 +201,7 @@ async function handleServiceStarted(id) {
|
||||
}
|
||||
|
||||
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 (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
|
||||
}
|
||||
|
||||
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) {
|
||||
if (msg.type === "log") {
|
||||
@ -243,21 +230,35 @@ async function handleIPCData(service_id, msg) {
|
||||
if (msg.type === "router:register") {
|
||||
if (msg.data.path_overrides) {
|
||||
for await (let pathOverride of msg.data.path_overrides) {
|
||||
await registerProxy(
|
||||
`/${pathOverride}`,
|
||||
`http://${internalIp}:${msg.data.listen.port}/${pathOverride}`,
|
||||
{
|
||||
await internal_proxy.register({
|
||||
serviceId: service_id,
|
||||
path: `/${pathOverride}`,
|
||||
target: `http://${internalIp}:${msg.data.listen.port}/${pathOverride}`,
|
||||
pathRewrite: {
|
||||
[`^/${pathOverride}`]: "",
|
||||
}
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
} else {
|
||||
await registerProxy(
|
||||
`/${service_id}`,
|
||||
`http://${msg.data.listen.ip}:${msg.data.listen.port}`
|
||||
)
|
||||
await internal_proxy.register({
|
||||
serviceId: service_id,
|
||||
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 }) {
|
||||
@ -276,11 +277,15 @@ function spawnService({ id, service, cwd }) {
|
||||
silent: true,
|
||||
cwd: cwd,
|
||||
env: instanceEnv,
|
||||
killSignal: "SIGKILL",
|
||||
})
|
||||
|
||||
instance.reload = () => {
|
||||
ipcRouter.unregister({ id, instance })
|
||||
|
||||
// try to unregister from proxy
|
||||
internal_proxy.unregisterAllFromService(id)
|
||||
|
||||
instance.kill()
|
||||
|
||||
instance = spawnService({ id, service, cwd })
|
||||
@ -340,31 +345,6 @@ function createServiceLogTransformer({ id, color = "bgCyan" }) {
|
||||
async function main() {
|
||||
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.log(comtyAscii)
|
||||
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") {
|
||||
const ignored = [
|
||||
...await getIgnoredFiles(cwd),
|
||||
"**/.cache/**",
|
||||
"**/node_modules/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
@ -438,7 +419,6 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// create repl
|
||||
repl.start({
|
||||
prompt: "> ",
|
||||
useGlobal: true,
|
||||
@ -474,11 +454,6 @@ async function main() {
|
||||
}
|
||||
})
|
||||
|
||||
await internal_proxy.listen({
|
||||
host: "0.0.0.0",
|
||||
port: 9000
|
||||
})
|
||||
|
||||
onExit((code, signal) => {
|
||||
console.clear()
|
||||
console.log(`\n🛑 Preparing to exit...`)
|
||||
@ -493,7 +468,11 @@ async function main() {
|
||||
console.log(`Killing ${instance.id} [${instance.instance.pid}]`)
|
||||
|
||||
instance.instance.kill()
|
||||
|
||||
treeKill(instance.instance.pid)
|
||||
}
|
||||
|
||||
treeKill(process.pid)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -24,14 +24,21 @@
|
||||
"clui": "^0.3.6",
|
||||
"dotenv": "^16.4.4",
|
||||
"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",
|
||||
"linebridge": "^0.18.1",
|
||||
"module-alias": "^2.2.3",
|
||||
"p-map": "^4.0.0",
|
||||
"p-queue": "^7.3.4",
|
||||
"radix3": "^1.1.1",
|
||||
"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": {
|
||||
"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.values.mfa_enabled) {
|
||||
if (userConfig.values["auth:mfa"]) {
|
||||
let codeVerified = false
|
||||
|
||||
// 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 DbManager from "@shared-classes/DbManager"
|
||||
import RedisClient from "@shared-classes/RedisClient"
|
||||
import StorageClient from "@shared-classes/StorageClient"
|
||||
import CacheService from "@shared-classes/CacheService"
|
||||
|
||||
import SharedMiddlewares from "@shared-middlewares"
|
||||
import LimitsClass from "@shared-classes/Limits"
|
||||
|
||||
class API extends Server {
|
||||
static refName = "files"
|
||||
@ -14,13 +16,12 @@ class API extends Server {
|
||||
static routesPath = `${__dirname}/routes`
|
||||
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3002
|
||||
|
||||
static maxBodyLength = 1000 * 1000 * 1000
|
||||
|
||||
middlewares = {
|
||||
...SharedMiddlewares
|
||||
}
|
||||
|
||||
contexts = {
|
||||
db: new DbManager(),
|
||||
cache: new CacheService(),
|
||||
redis: RedisClient(),
|
||||
storage: StorageClient(),
|
||||
@ -28,12 +29,19 @@ class API extends Server {
|
||||
applicationKeyId: process.env.B2_KEY_ID,
|
||||
applicationKey: process.env.B2_APP_KEY,
|
||||
}),
|
||||
limits: {},
|
||||
}
|
||||
|
||||
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.storage.initialize()
|
||||
await this.contexts.b2Storage.authorize()
|
||||
|
||||
this.contexts.limits = await LimitsClass.get()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,104 +1,54 @@
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
|
||||
import FileUpload from "@shared-classes/FileUpload"
|
||||
import PostProcess from "@services/post-process"
|
||||
import ChunkFileUpload from "@shared-classes/ChunkFileUpload"
|
||||
|
||||
import RemoteUpload from "@services/remoteUpload"
|
||||
|
||||
export default {
|
||||
useContext: ["cache", "storage", "b2Storage"],
|
||||
useContext: ["cache", "limits"],
|
||||
middlewares: [
|
||||
"withAuthentication",
|
||||
],
|
||||
fn: async (req, res) => {
|
||||
const { cache, storage, b2Storage } = this.default.contexts
|
||||
|
||||
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 maxFileSize = 10 * 1000 * 1000 * 1000
|
||||
const tmpPath = path.resolve(userPath)
|
||||
|
||||
// 10MB in bytes
|
||||
const maxChunkSize = 10 * 1000 * 1000
|
||||
let build = await ChunkFileUpload(req, {
|
||||
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)
|
||||
.catch((err) => {
|
||||
console.log("err", err)
|
||||
if (typeof build === "function") {
|
||||
try {
|
||||
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) {
|
||||
return false
|
||||
} else {
|
||||
if (typeof build === "function") {
|
||||
try {
|
||||
build = await build()
|
||||
fs.promises.rm(tmpPath, { recursive: true, force: true })
|
||||
|
||||
if (!req.headers["no-compression"]) {
|
||||
build = await PostProcess(build)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
fs.promises.rm(tmpPath, { recursive: true, force: true })
|
||||
|
||||
// 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 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)
|
||||
}
|
||||
throw new OperationError(error.code ?? 500, error.message ?? "Failed to upload file")
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
})
|
||||
return {
|
||||
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,
|
||||
} = options
|
||||
|
||||
const result = await videoTranscode(file.filepath, file.cachePath, {
|
||||
const result = await videoTranscode(file.filepath, {
|
||||
videoCodec,
|
||||
format,
|
||||
audioBitrate,
|
||||
videoBitrate: [videoBitrate, true],
|
||||
extraOptions: [
|
||||
"-threads 1"
|
||||
]
|
||||
})
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
export default (input, cachePath, params = defaultParams) => {
|
||||
const maxTasks = 5
|
||||
|
||||
export default (input, params = defaultParams) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const filename = path.basename(input)
|
||||
const outputFilename = `${filename.split(".")[0]}_ff.${params.format ?? "webm"}`
|
||||
const outputFilepath = `${cachePath}/${outputFilename}`
|
||||
if (!global.ffmpegTasks) {
|
||||
global.ffmpegTasks = []
|
||||
}
|
||||
|
||||
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}`)
|
||||
|
||||
@ -22,8 +31,8 @@ export default (input, cachePath, params = defaultParams) => {
|
||||
console.debug(`[TRANSCODING] Finished transcode ${input} to ${outputFilepath}`)
|
||||
|
||||
return resolve({
|
||||
filepath: outputFilepath,
|
||||
filename: outputFilename,
|
||||
filepath: outputFilepath,
|
||||
})
|
||||
}
|
||||
|
||||
@ -42,22 +51,33 @@ export default (input, cachePath, params = defaultParams) => {
|
||||
}
|
||||
|
||||
// chain methods
|
||||
Object.keys(commands).forEach((key) => {
|
||||
for (let key in commands) {
|
||||
if (exec === null) {
|
||||
exec = ffmpeg(commands[key])
|
||||
} else {
|
||||
if (typeof exec[key] !== "function") {
|
||||
console.warn(`[TRANSCODING] Method ${key} is not a function`)
|
||||
return false
|
||||
continue
|
||||
}
|
||||
|
||||
if (key === "extraOptions" && Array.isArray(commands[key])) {
|
||||
for (const option of commands[key]) {
|
||||
exec = exec.inputOptions(option)
|
||||
}
|
||||
|
||||
if (Array.isArray(commands[key])) {
|
||||
exec = exec[key](...commands[key])
|
||||
} else {
|
||||
exec = exec[key](commands[key])
|
||||
}
|
||||
continue
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
.on("error", onError)
|
||||
|
@ -1,28 +1,16 @@
|
||||
import { Server } from "linebridge/src/server"
|
||||
|
||||
import { Config, User } from "@db_models"
|
||||
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"
|
||||
|
||||
export default class API extends Server {
|
||||
static refName = "main"
|
||||
static useEngine = "hyper-express"
|
||||
static routesPath = `${__dirname}/routes`
|
||||
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 = {
|
||||
...require("@middlewares").default,
|
||||
@ -31,102 +19,16 @@ export default class API extends Server {
|
||||
|
||||
events = require("./events")
|
||||
|
||||
storage = global.storage = StorageClient()
|
||||
DB = new DbManager()
|
||||
contexts = {
|
||||
db: new DbManager(),
|
||||
}
|
||||
|
||||
async onInitialize() {
|
||||
await this.DB.initialize()
|
||||
await this.storage.initialize()
|
||||
|
||||
await this.initializeConfigDB()
|
||||
await this.checkSetup()
|
||||
await this.contexts.db.initialize()
|
||||
await StartupDB()
|
||||
}
|
||||
|
||||
initializeConfigDB = async () => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
handleWsAuth = require("@shared-lib/handleWsAuth").default
|
||||
}
|
||||
|
||||
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 {
|
||||
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 getLiked = require("./methods/getLiked").default
|
||||
static getSaved = require("./methods/getSaved").default
|
||||
@ -10,4 +11,7 @@ export default class Posts {
|
||||
static toggleLike = require("./methods/toggleLike").default
|
||||
static report = require("./methods/report").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 { Post } from "@db_models"
|
||||
import fullfill from "./fullfill"
|
||||
|
||||
export default async (payload = {}) => {
|
||||
await requiredFields(["user_id"], payload)
|
||||
@ -32,9 +33,13 @@ export default async (payload = {}) => {
|
||||
|
||||
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
|
||||
}
|
@ -1,16 +1,23 @@
|
||||
import { Post } from "@db_models"
|
||||
import fullfillPostsData from "./fullfill"
|
||||
|
||||
const maxLimit = 300
|
||||
|
||||
export default async (payload = {}) => {
|
||||
let {
|
||||
for_user_id,
|
||||
post_id,
|
||||
query = {},
|
||||
skip = 0,
|
||||
trim = 0,
|
||||
limit = 20,
|
||||
sort = { created_at: -1 },
|
||||
} = payload
|
||||
|
||||
// set a hard limit on the number of posts to retrieve, used for pagination
|
||||
if (limit > maxLimit) {
|
||||
limit = maxLimit
|
||||
}
|
||||
|
||||
let posts = []
|
||||
|
||||
if (post_id) {
|
||||
@ -24,7 +31,7 @@ export default async (payload = {}) => {
|
||||
} else {
|
||||
posts = await Post.find({ ...query })
|
||||
.sort(sort)
|
||||
.skip(skip)
|
||||
.skip(trim)
|
||||
.limit(limit)
|
||||
}
|
||||
|
||||
@ -32,7 +39,6 @@ export default async (payload = {}) => {
|
||||
posts = await fullfillPostsData({
|
||||
posts,
|
||||
for_user_id,
|
||||
skip,
|
||||
})
|
||||
|
||||
// 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 {
|
||||
for_user_id,
|
||||
user_id,
|
||||
skip,
|
||||
trim,
|
||||
limit,
|
||||
} = payload
|
||||
|
||||
@ -14,8 +14,8 @@ export default async (payload = {}) => {
|
||||
|
||||
return await GetData({
|
||||
for_user_id: for_user_id,
|
||||
skip,
|
||||
limit,
|
||||
trim: trim,
|
||||
limit: limit,
|
||||
query: {
|
||||
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 = {}) => {
|
||||
let {
|
||||
@ -14,33 +14,26 @@ export default async (payload = {}) => {
|
||||
return []
|
||||
}
|
||||
|
||||
let savedPostsIds = []
|
||||
let postsSavesIds = []
|
||||
|
||||
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 })
|
||||
|
||||
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({
|
||||
_id: {
|
||||
$in: posts.map((post) => post.user_id)
|
||||
}
|
||||
})
|
||||
.select("-email")
|
||||
.select("-birthday"),
|
||||
}).catch(() => { }),
|
||||
PostLike.find({
|
||||
post_id: {
|
||||
$in: posts.map((post) => post._id)
|
||||
}
|
||||
}).catch(() => []),
|
||||
Comment.find({
|
||||
parent_id: {
|
||||
$in: posts.map((post) => post._id)
|
||||
}
|
||||
}).catch(() => []),
|
||||
])
|
||||
|
||||
// wrap likesData by post_id
|
||||
@ -54,19 +47,10 @@ export default async (payload = {}) => {
|
||||
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) => {
|
||||
post = post.toObject()
|
||||
if (typeof post.toObject === "function") {
|
||||
post = post.toObject()
|
||||
}
|
||||
|
||||
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()] ?? []
|
||||
|
||||
post.countLikes = likes.length
|
||||
|
||||
let comments = commentsData[post._id.toString()] ?? []
|
||||
|
||||
post.countComments = comments.length
|
||||
|
||||
if (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 {
|
||||
...post,
|
||||
comments: comments.map((comment) => comment._id.toString()),
|
||||
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