Merge pull request #123 from ragestudio/dev

1.37.0
This commit is contained in:
srgooglo 2025-04-10 20:26:28 +02:00 committed by GitHub
commit 17c54a62d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 598 additions and 6231 deletions

@ -1 +1 @@
Subproject commit 6c7a218716a8de758ad34543301f958f0826fe09 Subproject commit bce0abd21b00e0bcc163e612cb603e88cf8c02f3

View File

@ -1,41 +1,43 @@
import copyToClipboard from "@utils/copyToClipboard"
import pasteFromClipboard from "@utils/pasteFromClipboard"
export default { export default {
"default-context": (items) => { "default-context": (items) => {
const text = window.getSelection().toString() const text = window.getSelection().toString()
if (text) { if (text) {
items.push({ items.push({
label: "Copy", label: "Copy",
icon: "FiCopy", icon: "FiCopy",
action: (clickedItem, ctx) => { action: (clickedItem, ctx) => {
copyToClipboard(text) copyToClipboard(text)
ctx.close() ctx.close()
} },
}) })
} }
items.push({ items.push({
label: "Paste", label: "Paste",
icon: "FiClipboard", icon: "FiClipboard",
action: (clickedItem, ctx) => { action: (clickedItem, ctx) => {
app.message.error("This action is not supported by your browser") pasteFromClipboard(clickedItem)
ctx.close()
},
})
ctx.close() items.push({
} label: "Report a bug",
}) icon: "FiAlertTriangle",
action: (clickedItem, ctx) => {
app.eventBus.emit("app.reportBug", {
clickedItem,
})
items.push({ ctx.close()
label: "Report a bug", },
icon: "FiAlertTriangle", })
action: (clickedItem, ctx) => {
app.eventBus.emit("app.reportBug", {
clickedItem,
})
ctx.close() return items
} },
}) }
return items
}
}

View File

@ -2,69 +2,73 @@ import copyToClipboard from "@utils/copyToClipboard"
import download from "@utils/download" import download from "@utils/download"
export default { export default {
"post-card": (items, parent, element, control) => { "post-card": (items, parent, element, control) => {
items.push({ if (!parent.id) {
label: "Copy ID", parent = parent.parentNode
icon: "FiCopy", }
action: () => {
copyToClipboard(parent.id)
control.close()
}
})
items.push({ items.push({
label: "Copy Link", label: "Copy ID",
icon: "FiLink", icon: "FiCopy",
action: () => { action: () => {
copyToClipboard(`${window.location.origin}/post/${parent.id}`) copyToClipboard(parent.id)
control.close() control.close()
} },
}) })
let media = null items.push({
label: "Copy Link",
icon: "FiLink",
action: () => {
copyToClipboard(`${window.location.origin}/post/${parent.id}`)
control.close()
},
})
if (parent.querySelector(".attachment")) { let media = null
media = parent.querySelector(".attachment")
media = media.querySelector("video, img")
if (media.querySelector("source")) { if (parent.querySelector(".attachment")) {
media = media.querySelector("source") media = parent.querySelector(".attachment")
} media = media.querySelector("video, img")
}
if (media) { if (media.querySelector("source")) {
items.push({ media = media.querySelector("source")
type: "separator", }
}) }
items.push({ if (media) {
label: "Copy media URL", items.push({
icon: "FiCopy", type: "separator",
action: () => { })
copyToClipboard(media.src)
control.close()
}
})
items.push({ items.push({
label: "Open media in new tab", label: "Copy media URL",
icon: "FiExternalLink", icon: "FiCopy",
action: () => { action: () => {
window.open(media.src, "_blank") copyToClipboard(media.src)
control.close() control.close()
} },
}) })
items.push({ items.push({
label: "Download media", label: "Open media in new tab",
icon: "FiDownload", icon: "FiExternalLink",
action: () => { action: () => {
download(media.src) window.open(media.src, "_blank")
control.close() control.close()
} },
}) })
}
return items items.push({
} label: "Download media",
} icon: "FiDownload",
action: () => {
download(media.src)
control.close()
},
})
}
return items
},
}

View File

@ -1,15 +1,14 @@
{ {
"name": "@comty/app", "name": "@comty/app",
"version": "1.36.0@alpha", "version": "1.37.0@alpha",
"license": "ComtyLicense", "license": "ComtyLicense",
"main": "electron/main", "main": "electron/main",
"type": "module", "type": "module",
"author": "RageStudio", "author": "RageStudio",
"description": "A prototype of a social network.", "description": "A prototype of a social network.",
"scripts": { "scripts": {
"build": "vite build",
"dev": "vite", "dev": "vite",
"docker-compose:update_run": "docker-compose down && git pull && yarn build && docker-compose up -d --build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"release": "node ./scripts/release.js" "release": "node ./scripts/release.js"
}, },
@ -33,7 +32,7 @@
"axios": "^1.7.7", "axios": "^1.7.7",
"bear-react-carousel": "^4.0.10-alpha.0", "bear-react-carousel": "^4.0.10-alpha.0",
"classnames": "2.3.1", "classnames": "2.3.1",
"comty.js": "^0.61.0", "comty.js": "^0.63.0",
"dashjs": "^4.7.4", "dashjs": "^4.7.4",
"dompurify": "^3.0.0", "dompurify": "^3.0.0",
"fast-average-color": "^9.2.0", "fast-average-color": "^9.2.0",

View File

@ -1,3 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +0,0 @@
[package]
name = "comty"
version = "0.1.0"
description = "Comty Desktop APP"
authors = ["RageStudio"]
license = ""
repository = ""
default-run = "comty"
edition = "2021"
rust-version = "1.60"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.5.1", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.6.7", features = [ "api-all"] }
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]

View File

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1,14 +0,0 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![my_custom_command])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[tauri::command]
fn my_custom_command() {
println!("I was invoked from JS!");
}

View File

@ -1,78 +0,0 @@
{
"build": {
"devPath": "https://fr01.ragestudio.net:8000",
"distDir": "../dist"
},
"package": {
"productName": "Comty",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": true,
"window": {
"all": true,
"close": true,
"hide": true,
"show": true,
"maximize": true,
"minimize": true,
"unmaximize": true,
"unminimize": true,
"startDragging": true
}
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": []
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.ragestudio.comty",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"title": "Comty",
"titleBarStyle": "Overlay",
"hiddenTitle": true,
"decorations": true,
"fullscreen": false,
"width": 1280,
"height": 720,
"resizable": true,
"center": true
}
]
}
}

View File

@ -335,10 +335,17 @@ export default class PostCreator extends React.Component {
} }
handleOnMentionSearch = lodash.debounce(async (value) => { handleOnMentionSearch = lodash.debounce(async (value) => {
const results = await SearchModel.userSearch(`username:${value}`) if (value === "") {
return false
}
const results = await SearchModel.search(`${value}`, {
fields: "users",
limit: 5,
})
this.setState({ this.setState({
mentionsLoadedData: results, mentionsLoadedData: results.users.items,
}) })
}, 300) }, 300)
@ -674,13 +681,20 @@ export default class PostCreator extends React.Component {
draggable={false} draggable={false}
prefix="@" prefix="@"
allowClear allowClear
onBlur={() => {
this.setState({ mentionsLoadedData: [] })
}}
options={this.state.mentionsLoadedData.map((item) => { options={this.state.mentionsLoadedData.map((item) => {
return { return {
key: item.id, key: item.id,
value: item.username, value: item.username,
label: ( label: (
<> <>
<antd.Avatar src={item.avatar} /> <antd.Avatar
size={24}
src={item.avatar}
shape="square"
/>
<span>{item.username}</span> <span>{item.username}</span>
</> </>
), ),

View File

@ -9,13 +9,12 @@ const ContextMenu = (props) => {
const [visible, setVisible] = React.useState(true) const [visible, setVisible] = React.useState(true)
const { items = [], cords, clickedComponent, ctx } = props const { items = [], cords, clickedComponent, ctx } = props
async function onClose() {
setVisible(false)
props.unregisterOnClose(onClose)
}
React.useEffect(() => { React.useEffect(() => {
props.registerOnClose(onClose) if (props.fireWhenClosing) {
props.fireWhenClosing(() => {
setVisible(false)
})
}
}, []) }, [])
const handleItemClick = async (item) => { const handleItemClick = async (item) => {

View File

@ -1,158 +1,211 @@
import React from "react" import React from "react"
import { Core, EventBus } from "@ragestudio/vessel" import { Core, EventBus } from "@ragestudio/vessel"
import ContextMenu from "./components/contextMenu" import ContextMenu from "./components/contextMenu"
import DefaultContext from "@config/context-menu/default"
import DefaultContenxt from "@config/context-menu/default"
import PostCardContext from "@config/context-menu/post" import PostCardContext from "@config/context-menu/post"
export default class ContextMenuCore extends Core { export default class ContextMenuCore extends Core {
static namespace = "contextMenu" static namespace = "contextMenu"
contexts = { contexts = {
...DefaultContenxt, ...DefaultContext,
...PostCardContext, ...PostCardContext,
} }
eventBus = new EventBus() eventBus = new EventBus()
isMenuOpen = false
fireWhenClosing = null
async onInitialize() { async onInitialize() {
if (app.isMobile) { if (app.isMobile) {
this.console.warn("Context menu is not available on mobile") this.console.warn("Context menu is not available on mobile")
return false return false
} }
document.addEventListener("contextmenu", this.handleEvent.bind(this)) document.addEventListener("contextmenu", this.handleEvent)
} }
async handleEvent(event) { handleEvent = async (event) => {
event.preventDefault() event.preventDefault()
// get the cords of the mouse // obtain cord of mouse
const x = event.clientX const x = event.clientX
const y = event.clientY const y = event.clientY
// get the component that was clicked // get clicked component
const component = document.elementFromPoint(x, y) const component = document.elementFromPoint(x, y)
// check if is clicking inside a context menu or a children inside a context menu // check if right-clicked inside a context menu
if (component.classList.contains("contextMenu") || component.closest(".contextMenu")) { if (
return component.classList.contains("contextMenu") ||
} component.closest(".contextMenu")
) {
return
}
const items = await this.generateItems(component) // gen items
const items = await this.generateItems(component)
if (!items) { // if no items, abort
this.console.warn("No context menu items found, aborting") if (!items || items.length === 0) {
return false this.console.error("No context menu items found, aborting")
} return false
}
this.show({ // render menu
registerOnClose: (cb) => { this.eventBus.on("close", cb) }, this.show({
unregisterOnClose: (cb) => { this.eventBus.off("close", cb) }, cords: { x, y },
cords: { clickedComponent: component,
x, items: items,
y, fireWhenClosing: (fn) => {
}, this.fireWhenClosing = fn
clickedComponent: component, },
items: items, ctx: {
ctx: { close: this.close,
close: this.onClose, },
} })
}) }
}
registerContext = async (element, context) => { registerContext = (element, context) => {
this.contexts[element] = context this.contexts[element] = context
} }
generateItems = async (element) => { generateItems = async (element) => {
let items = [] let contextNames = []
let finalItems = []
// find the closest context with attribute (context-menu) // search parent element with context-menu attribute
// if not found, use default context const parentElement = element.closest("[context-menu]")
const parentElement = element.closest("[context-menu]")
let contexts = [] // if parent element exists, get context names from attribute
if (parentElement) {
const contextAttr = parentElement.getAttribute("context-menu") || ""
contextNames = contextAttr
.split(",")
.map((context) => context.trim())
if (parentElement) { // if context includes "ignore", no show context menu
contexts = parentElement.getAttribute("context-menu") ?? [] if (contextNames.includes("ignore")) {
return null
}
}
if (typeof contexts === "string") { // if context includes "no-default", no add default context
contexts = contexts.split(",").map((context) => context.trim()) if (!contextNames.includes("no-default")) {
} contextNames.push("default-context")
} } else {
// remove "no-default" from context names
contextNames = contextNames.filter(
(context) => context !== "no-default",
)
}
// if context includes ignore, return null // process each context sequentially
if (contexts.includes("ignore")) { for (let i = 0; i < contextNames.length; i++) {
return null const contextName = contextNames[i]
}
// check if context includes no-default, if not, push default context and remove no-default // obtain contexted items
if (contexts.includes("no-default")) { const contextItems = await this.getContextItems(
contexts = contexts.filter((context) => context !== "no-default") contextName,
} else { parentElement,
contexts.push("default-context") element,
} )
for await (const [index, context] of contexts.entries()) { // if any contexted items exist, add them to the final items
let contextObject = this.contexts[context] if (contextItems && contextItems.length > 0) {
finalItems = finalItems.concat(contextItems)
if (!contextObject) { // if is not the last context, add a separator
this.console.warn(`Context ${context} not found`) if (i < contextNames.length - 1) {
continue finalItems.push({
} type: "separator",
})
}
}
}
if (typeof contextObject === "function") { // assign indices
contextObject = await contextObject(items, parentElement, element, { finalItems = finalItems.map((item, index) => {
close: this.onClose, if (!item.index) {
}) item.index = index
} }
return item
})
// push divider // sort items by index
if (contexts.length > 0 && index !== contexts.length - 1) { finalItems.sort((a, b) => a.index - b.index)
items.push({
type: "separator"
})
}
}
// fullfill each item with a correspondent index if missing declared // remove undefined items
items = items.map((item, index) => { finalItems = finalItems.filter((item) => item !== undefined)
if (!item.index) {
item.index = index
}
return item return finalItems
}) }
// short items (if has declared index) getContextItems = async (contextName, parentElement, element) => {
items = items.sort((a, b) => a.index - b.index) const contextObject = this.contexts[contextName]
// remove undefined items if (!contextObject) {
items = items.filter((item) => item !== undefined) this.console.warn(`Context ${contextName} not found`)
return []
}
return items // if is a function, execute it to get the elements
} if (typeof contextObject === "function") {
try {
const newItems = []
show = async (payload) => { // call the function
app.cores.window_mng.render( const result = await contextObject(
"context-menu-portal", newItems,
React.createElement(ContextMenu, payload), parentElement,
{ element,
onClose: this.onClose, { close: this.close },
createOrUpdate: true, )
closeOnClickOutside: true,
},
)
}
onClose = async (delay = 200) => { return result || newItems
this.eventBus.emit("close", delay) } catch (error) {
this.console.error(
`Error processing context [${contextName}] >`,
error,
)
return []
}
}
await new Promise((resolve) => { // if it is an object (array), return it directly
setTimeout(resolve, delay) return Array.isArray(contextObject) ? contextObject : []
}) }
}
} show = async (props) => {
app.cores.window_mng.render(
"context-menu-portal",
React.createElement(ContextMenu, props),
{
useFrame: false,
createOrUpdate: true,
closeOnClickOutside: true, // sets default click outside behavior
onClose: this.onClose, // triggered when the menu is closing
},
)
this.isMenuOpen = true
}
// triggered when the menu is closing
onClose = async (delay = 200) => {
if (typeof this.fireWhenClosing === "function") {
await this.fireWhenClosing()
}
if (delay > 0) {
await new Promise((resolve) => setTimeout(resolve, delay))
}
this.isMenuOpen = false
}
// close the menu
close = async () => {
app.cores.window_mng.close("context-menu-portal")
}
}

View File

@ -11,7 +11,8 @@ const variantToAlgorithm = {
dark: theme.darkAlgorithm, dark: theme.darkAlgorithm,
} }
const ClientPrefersDark = () => window.matchMedia("(prefers-color-scheme: dark)") const ClientPrefersDark = () =>
window.matchMedia("(prefers-color-scheme: dark)")
function variantKeyToColor(key) { function variantKeyToColor(key) {
if (key == "auto") { if (key == "auto") {
@ -25,7 +26,6 @@ function variantKeyToColor(key) {
return key return key
} }
export class ThemeProvider extends React.Component { export class ThemeProvider extends React.Component {
state = { state = {
useAlgorigthm: variantKeyToColor(app.cores.style.currentVariantKey), useAlgorigthm: variantKeyToColor(app.cores.style.currentVariantKey),
@ -37,7 +37,7 @@ export class ThemeProvider extends React.Component {
this.setState({ this.setState({
useAlgorigthm: variantKeyToColor(app.cores.style.currentVariantKey), useAlgorigthm: variantKeyToColor(app.cores.style.currentVariantKey),
useCompactMode: update["compact-mode"] useCompactMode: update["compact-mode"],
}) })
} }
@ -58,19 +58,19 @@ export class ThemeProvider extends React.Component {
themeAlgorithms.push(theme.compactAlgorithm) themeAlgorithms.push(theme.compactAlgorithm)
} }
return <ConfigProvider return (
theme={{ <ConfigProvider
token: { theme={{
...app.cores.style.getVar(), token: {
}, ...app.cores.style.getVar(),
algorithm: themeAlgorithms, },
}} algorithm: themeAlgorithms,
componentSize={ }}
app.isMobile ? "large" : "middle" componentSize={app.isMobile ? "large" : "middle"}
} >
> {this.props.children}
{this.props.children} </ConfigProvider>
</ConfigProvider> )
} }
} }
@ -83,9 +83,12 @@ export default class StyleCore extends Core {
static defaultVariantKey = "auto" static defaultVariantKey = "auto"
static get rootVariables() { static get rootVariables() {
let attributes = document.documentElement.getAttribute("style").trim().split(";") let attributes = document.documentElement
.getAttribute("style")
.trim()
.split(";")
attributes = attributes.slice(0, (attributes.length - 1)) attributes = attributes.slice(0, attributes.length - 1)
attributes = attributes.map((variable) => { attributes = attributes.map((variable) => {
let [key, value] = variable.split(":") let [key, value] = variable.split(":")
@ -107,7 +110,7 @@ export default class StyleCore extends Core {
isOnTemporalVariant = false isOnTemporalVariant = false
// modifications // modifications
static get storagedModifications() { static get storagedModifications() {
return store.get(StyleCore.modificationStorageKey) ?? {} return store.get(StyleCore.modificationStorageKey) ?? {}
} }
@ -149,12 +152,14 @@ export default class StyleCore extends Core {
} }
// apply variation // apply variation
this.applyVariant(StyleCore.storagedVariantKey ?? StyleCore.defaultVariantKey) this.applyVariant(
StyleCore.storagedVariantKey ?? StyleCore.defaultVariantKey,
)
// if mobile set fontScale to 1 // if mobile set fontScale to 1
if (app.isMobile) { if (app.isMobile) {
this.applyStyles({ this.applyStyles({
fontScale: 1 fontScale: 1,
}) })
} }
@ -181,7 +186,10 @@ export default class StyleCore extends Core {
} }
} }
return StyleCore.storagedModifications[key] || this.public.theme.defaultVars[key] return (
StyleCore.storagedModifications[key] ||
this.public.theme.defaultVars[key]
)
} }
getDefaultVar(key) { getDefaultVar(key) {
@ -201,11 +209,14 @@ export default class StyleCore extends Core {
this.public.mutation = { this.public.mutation = {
...this.public.theme.defaultVars, ...this.public.theme.defaultVars,
...this.public.mutation, ...this.public.mutation,
...update ...update,
} }
Object.keys(this.public.mutation).forEach(key => { Object.keys(this.public.mutation).forEach((key) => {
document.documentElement.style.setProperty(`--${key}`, this.public.mutation[key]) document.documentElement.style.setProperty(
`--${key}`,
this.public.mutation[key],
)
}) })
app.eventBus.emit("style.update", { app.eventBus.emit("style.update", {
@ -213,7 +224,10 @@ export default class StyleCore extends Core {
}) })
} }
applyVariant = (variantKey = (this.public.theme.defaultVariant ?? "light"), save = true) => { applyVariant = (
variantKey = this.public.theme.defaultVariant ?? "light",
save = true,
) => {
if (save) { if (save) {
StyleCore.storagedVariantKey = variantKey StyleCore.storagedVariantKey = variantKey
this.public.currentVariantKey = variantKey this.public.currentVariantKey = variantKey
@ -253,8 +267,11 @@ export default class StyleCore extends Core {
resetToDefault() { resetToDefault() {
store.remove(StyleCore.modificationStorageKey) store.remove(StyleCore.modificationStorageKey)
app.cores.settings.set("colorPrimary", this.public.theme.defaultVars.colorPrimary) app.cores.settings.set(
"colorPrimary",
this.public.theme.defaultVars.colorPrimary,
)
this.onInitialize() this.onInitialize()
} }
} }

View File

@ -158,15 +158,15 @@ export default class WindowManager extends Core {
}) })
if (!win || !element) { if (!win || !element) {
this.console.error(`Window [${id}] not found`) this.console.error(`[${id}] Window not found`)
return false return false
} }
this.console.debug(`Closing window ${id}`, win, element) this.console.debug(`[${id}] Closing window`, win, element)
// if onClose callback is defined, call it // if onClose callback is defined, call it
if (typeof win.onClose === "function") { if (typeof win.onClose === "function") {
this.console.debug(`Trigging close callback for window ${id}`) this.console.debug(`[${id}] Trigging on closing callback`)
await win.onClose() await win.onClose()
} }

View File

@ -45,7 +45,7 @@ const ProfileData = (props) => {
async function handleChange(key, value) { async function handleChange(key, value) {
setLoading(true) setLoading(true)
const result = await Streaming.createOrUpdateStream({ const result = await Streaming.createOrUpdateProfile({
[key]: value, [key]: value,
_id: profile._id, _id: profile._id,
}).catch((error) => { }).catch((error) => {

View File

@ -1,6 +1,7 @@
import React from "react" import React from "react"
import * as antd from "antd" import * as antd from "antd"
import { FastAverageColor } from "fast-average-color" import { FastAverageColor } from "fast-average-color"
import { Icons } from "@components/Icons"
import UserPreview from "@components/UserPreview" import UserPreview from "@components/UserPreview"
@ -69,17 +70,26 @@ const LivestreamItem = (props) => {
<div className="livestream_info"> <div className="livestream_info">
<div className="livestream_titles"> <div className="livestream_titles">
<antd.Tag className="livestream_category">
{livestream.info?.category}
</antd.Tag>
<div className="livestream_title"> <div className="livestream_title">
<h1>{livestream.info?.title}</h1> <h1>{livestream.info?.title}</h1>
</div> </div>
<div className="livestream_description"> <div className="livestream_description">
<h2> <span className="livestream_description-text">
{livestream.info?.description ?? "No description"} {livestream.info?.description ?? "No description"}
</h2> </span>
</div> </div>
</div> </div>
<div className="livestream_views">
<Icons.FiEye />
<h4>{livestream.info?.viewers ?? 0}</h4>
</div>
<UserPreview user={livestream.user} small /> <UserPreview user={livestream.user} small />
</div> </div>
</div> </div>

View File

@ -1,120 +1,170 @@
@item_border_radius: 10px; @item_border_radius: 10px;
.livestream_list { .livestream_list {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
grid-column-gap: 15px; grid-column-gap: 15px;
grid-row-gap: 15px; grid-row-gap: 15px;
width: 100%; width: 100%;
height: 100%; height: 100%;
@media not screen and (max-width: 1900px) { @media not screen and (max-width: 1900px) {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
@media not screen and (max-width: 2300px) { @media not screen and (max-width: 2300px) {
grid-template-columns: repeat(5, minmax(0, 1fr)); grid-template-columns: repeat(5, minmax(0, 1fr));
} }
@media not screen and (max-width: 2600px) { @media not screen and (max-width: 2600px) {
grid-template-columns: repeat(6, minmax(0, 1fr)); grid-template-columns: repeat(6, minmax(0, 1fr));
} }
@media not screen and (max-width: 2900px) { @media not screen and (max-width: 2900px) {
grid-template-columns: repeat(7, minmax(0, 1fr)); grid-template-columns: repeat(7, minmax(0, 1fr));
} }
&.empty { &.empty {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
}
.livestream_item { .livestream_item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 100%; width: 100%;
height: fit-content; height: fit-content;
padding: 10px; padding: 10px;
transition: all 150ms ease-in-out; transition: all 150ms ease-in-out;
background-color: var(--background-color-primary-2); background-color: var(--background-color-primary-2);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: @item_border_radius; border-radius: @item_border_radius;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: var(--background-color-accent); background-color: var(--background-color-accent);
} }
&.white_background { &.white_background {
h1,
h2,
span {
color: var(--text-color-black) !important;
}
}
h1, .livestream_thumbnail {
h2, width: 100%;
span {
color: var(--text-color-black) !important;
}
}
.livestream_thumbnail { img {
width: 100%; width: 100%;
height: 100%;
img { border-radius: @item_border_radius;
width: 100%;
height: 100%;
border-radius: @item_border_radius; object-fit: cover;
}
}
object-fit: cover; .livestream_info {
} position: relative;
}
.livestream_info { display: flex;
position: relative; flex-direction: column;
display: flex; width: 100%;
flex-direction: column;
width: 100%; gap: 10px;
gap: 10px; h1,
h2 {
margin: 0;
}
h1, .livestream_titles {
h2 { position: relative;
margin: 0;
}
.livestream_titles { display: flex;
display: flex; flex-direction: column;
flex-direction: column;
gap: 7px; padding: 10px 0;
color: var(--text-color); color: var(--text-color);
.livestream_title { .livestream_title {
margin-top: 10px; max-height: 200px;
font-size: 1rem;
height: fit-content;
font-family: "Space Grotesk", sans-serif;
}
.livestream_description { overflow: hidden;
font-size: 0.6rem; white-space: wrap;
font-weight: 400; text-overflow: ellipsis;
height: fit-content;
} h1 {
} font-size: 1.4rem;
} font-family: "Space Grotesk", sans-serif;
} text-overflow: ellipsis;
} }
}
.livestream_description {
display: flex;
flex-direction: row;
align-items: center;
max-height: 200px;
overflow: hidden;
white-space: wrap;
text-overflow: ellipsis;
.livestream_description-text {
font-size: 0.7rem;
font-weight: 400;
}
}
.livestream_category {
position: absolute;
top: 0;
right: 0;
margin-top: 13px;
margin-left: 0;
margin-right: 0;
}
}
.livestream_views {
position: absolute;
bottom: 0;
right: 0;
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
padding: 5px 10px;
background-color: var(--background-color-primary);
border-radius: 12px;
font-family: "DM Mono", monospace;
}
}
}

View File

@ -1,47 +0,0 @@
import React from "react"
import config from "@config"
class Splash extends React.Component {
state = {
visible: true
}
onUnmount = async () => {
this.setState({
visible: false
})
return await new Promise((resolve) => {
setTimeout(resolve, 1000)
})
}
render() {
return <div
className={this.state.visible ? "app_splash_wrapper" : "app_splash_wrapper fade-away"}
>
<div className="content">
<img
src={config.logo.alt}
/>
<div className="loader_wrapper">
<div
className="loader"
>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
</div>
}
}
export default Splash

View File

@ -686,3 +686,27 @@
} }
} }
} }
.ant-empty-image {
color: var(--text-color) !important;
}
.ant-empty-description {
color: var(--text-color) !important;
}
.ant-mentions-dropdown {
background-color: var(--background-color-primary);
color: var(--text-color);
}
.ant-mentions-dropdown-menu-item {
display: flex !important;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 5px !important;
color: var(--text-color);
}

View File

@ -1,8 +1,48 @@
export default (text) => { /**
if (!navigator.clipboard?.writeText) { * Copies content to clipboard, supporting both text and file data
return app.message.error("Clipboard API not supported") * @param {string|File|Blob} content - The content to copy (text string or file/blob data)
} * @param {Object} options - Optional configuration
* @param {string} options.successMessage - Custom success message
* @returns {Promise<boolean>} - Promise resolving to success state
*/
export default async (content, options = {}) => {
const { successMessage = "Copied to clipboard" } = options
navigator.clipboard.writeText(text) try {
app.message.success("Copied to clipboard") if (!navigator.clipboard) {
} app.message.error("Clipboard API not supported")
return false
}
if (typeof content === "string") {
await navigator.clipboard.writeText(content)
app.message.success(successMessage)
return true
}
if (content instanceof File || content instanceof Blob) {
const clipboardItem = new ClipboardItem({
[content.type]: content,
})
if (!navigator.clipboard.write) {
app.message.error("File copying not supported in this browser")
return false
}
await navigator.clipboard.write([clipboardItem])
app.message.success(successMessage)
return true
}
app.message.error("Unsupported content type")
return false
} catch (error) {
console.error("Clipboard operation failed:", error)
app.message.error("Failed to copy to clipboard")
return false
}
}

View File

@ -0,0 +1,33 @@
export async function pasteFromClipboard(element) {
if (!navigator.clipboard) {
throw new Error(
"Clipboard API not available in this browser or context",
)
}
if (!element || !(element instanceof HTMLElement)) {
console.error("Invalid element provided to pasteFromClipboard")
return Promise.reject(new Error("Invalid element provided"))
}
let data = await navigator.clipboard.read()
data = data[0]
data = await data.getType(data.types[0])
const event = new ClipboardEvent("paste", {
clipboardData: new DataTransfer(),
})
element.focus()
element.dispatchEvent(event)
return data
}
export function isClipboardSupported() {
return !!navigator.clipboard
}
export default pasteFromClipboard

View File

@ -1,38 +0,0 @@
const path = require("path")
const fs = require("fs")
const exec = require("child_process").execSync
const sharedRootPath = path.resolve(process.cwd(), "shared")
const rootPath = process.cwd()
const packagesPath = path.resolve(rootPath, "packages")
const getPackages = require("./utils/getPackages")
async function main() {
const packages = await getPackages()
// copy shared dir to each root package path
for await (const packageName of packages) {
const packagePath = path.resolve(packagesPath, packageName)
const sharedPath = path.resolve(packagePath, "src", "_shared")
if (fs.existsSync(sharedPath)) {
// remove old shared folder
fs.rmdirSync(sharedPath, { recursive: true })
}
// copy entire shared folder
// shared/* => /_shared/*
fs.mkdirSync(sharedPath, { recursive: true })
await exec(`cp -r ${sharedRootPath}/* ${sharedPath}`)
}
console.log("📦 Shared classes copied to each package.")
// run docker build
await exec("sudo docker compose build --no-cache")
}
main().catch(console.error)

View File

@ -1,61 +0,0 @@
require("dotenv").config()
const path = require("path")
const fs = require("fs")
const child_process = require("child_process")
const sharedRootPath = path.resolve(process.cwd(), "shared")
const rootPath = process.cwd()
const packagesPath = path.resolve(rootPath, "packages")
const getPackages = require("./utils/getPackages")
async function main() {
const packages = await getPackages({
ignore: ["shared", "app", "wrapper", "comty.js"]
})
for await (const packageName of packages) {
const packagePath = path.resolve(packagesPath, packageName)
// copy shared
const sharedPath = path.resolve(packagePath, "src", "_shared")
if (fs.existsSync(sharedPath)) {
// remove old shared folder
fs.rmdirSync(sharedPath, { recursive: true })
}
// copy entire shared folder
fs.mkdirSync(sharedPath, { recursive: true })
await child_process.execSync(`cp -r ${sharedRootPath}/* ${sharedPath} && echo Shared lib copied`, {
cwd: packagePath,
stdio: "inherit"
})
console.log(`Building [${packagePath}]`)
// run yarn build
await child_process.execSync("yarn build", {
cwd: packagePath,
stdio: "inherit"
})
if (process.env.INFISICAL_TOKEN) {
// write env
const envPath = path.resolve(packagePath, ".env")
if (fs.existsSync(envPath)) {
fs.unlinkSync(envPath)
}
const envData = `INFISICAL_TOKEN="${process.env.INFISICAL_TOKEN}"`
await fs.writeFileSync(envPath, envData)
}
}
}
main().catch(console.error)

View File

@ -1,196 +0,0 @@
const fs = require("node:fs")
const path = require("node:path")
const child_process = require("node:child_process")
const rootPath = process.cwd()
const sharedRootPath = path.resolve(rootPath, "shared")
const packagesPath = path.resolve(rootPath, "packages")
const getPackages = require("./utils/getPackages")
const pkgjson = require("../package.json")
// vars
const appPath = path.resolve(rootPath, pkgjson._web_app_path)
const comtyjsPath = path.resolve(rootPath, "comty.js")
const vesselPath = path.resolve(rootPath, "vessel")
const linebridePath = path.resolve(rootPath, "linebridge")
async function linkSharedResources(pkgJSON, packagePath) {
if (typeof pkgJSON !== "object") {
throw new Error("Package must be an object")
}
const { shared } = pkgJSON
if (!shared) {
return
}
if (typeof shared === "string") {
const finalLinkPath = path.resolve(packagePath, shared)
if (fs.existsSync(finalLinkPath)) {
console.warn(`⚠️ Resource [${shared}] link already exists in [${finalLinkPath}]`)
return
}
// link entire folder
fs.symlinkSync(sharedRootPath, finalLinkPath, "dir")
} else {
for (const [resource, linkPath] of Object.entries(shared)) {
const originClassPath = path.resolve(sharedRootPath, resource)
const finalLinkPath = path.resolve(packagePath, linkPath)
if (!fs.existsSync(originClassPath)) {
throw new Error(`Resource [${resource}] does not exist`)
}
if (fs.existsSync(finalLinkPath)) {
console.warn(`⚠️ Resource [${resource}] link already exists in [${finalLinkPath}]`)
continue
} else {
fs.mkdirSync(path.resolve(finalLinkPath, ".."), { recursive: true })
}
try {
fs.symlinkSync(originClassPath, finalLinkPath, "dir")
console.log(`🔗 Linked resouce [${resource}] to [${finalLinkPath}]`)
} catch (error) {
if (error.code && error.code == 'EEXIST') {
fs.unlinkSync(finalLinkPath)
fs.symlinkSync(originClassPath, finalLinkPath, "dir")
console.log(`🔗 Linked resouce [${resource}] to [${finalLinkPath}]`)
}
}
continue
}
}
}
async function linkInternalSubmodules(packages) {
//* APP RUNTIME LINKING
console.log(`Linking Vessel to app...`)
await child_process.execSync("yarn link", {
cwd: vesselPath,
stdio: "inherit",
})
await child_process.execSync(`yarn link "vessel"`, {
cwd: appPath,
stdio: "inherit",
})
//* COMTY.JS LINKING
console.log(`Linking comty.js to app...`)
await child_process.execSync(`yarn link`, {
cwd: comtyjsPath,
stdio: "inherit",
})
await child_process.execSync(`yarn link "comty.js"`, {
cwd: appPath,
stdio: "inherit",
})
//* LINEBRIDE LINKING
console.log(`Linking Linebride to servers...`)
await child_process.execSync(`yarn link`, {
cwd: linebridePath,
stdio: "inherit",
})
for await (const packageName of packages) {
const packagePath = path.resolve(packagesPath, packageName)
const packageJsonPath = path.resolve(packagePath, "package.json")
if (!fs.existsSync(packageJsonPath)) {
continue
}
await child_process.execSync(`yarn link "linebridge"`, {
cwd: packagePath,
stdio: "inherit",
})
console.log(`Linking Linebride to package [${packageName}]...`)
}
console.log(`✅ All submodules linked!`)
return true
}
async function main() {
console.time("✅ post-install tooks:")
// read dir with absolute paths
let packages = await getPackages()
// link shared resources
for (const packageName of packages) {
const packagePath = path.resolve(packagesPath, packageName)
const packageJsonPath = path.resolve(packagePath, "package.json")
if (!fs.existsSync(packageJsonPath)) {
continue
}
const packageJson = require(packageJsonPath)
if (packageJson.shared) {
console.log(`📦 Package [${packageName}] has declared shared resources.`)
await linkSharedResources(packageJson, packagePath)
}
}
// install dependencies for modules
console.log("Installing app dependencies...")
await child_process.execSync("npm install --force", {
cwd: appPath,
stdio: "inherit",
})
console.log("Installing vessel dependencies...")
await child_process.execSync("npm install --force", {
cwd: vesselPath,
stdio: "inherit",
})
console.log("Installing comty.js dependencies...")
await child_process.execSync("npm install --force", {
cwd: comtyjsPath,
stdio: "inherit",
})
console.log("Installing linebridge dependencies...")
await child_process.execSync("npm install --force", {
cwd: linebridePath,
stdio: "inherit",
})
// link internal submodules
await linkInternalSubmodules(packages)
// fixes for arm architecture
if (process.arch == "arm64") {
// rebuild tfjs
console.log("Rebuilding TFJS...")
await child_process.execSync("npm rebuild @tensorflow/tfjs-node --build-from-source", {
cwd: rootPath,
stdio: "inherit",
})
}
console.timeEnd("✅ post-install tooks:")
}
main().catch(console.error)

View File

@ -1,135 +0,0 @@
#!/usr/bin/env node
import path from "path"
import fs from "fs"
import axios from "axios"
import sevenzip from "7zip-min"
import formdata from "form-data"
const marketplaceAPIOrigin = "https://indev.comty.app/api/extensions"
const token = process.argv[2]
const excludedFiles = [
"/.git",
"/.tmp",
"/bundle.7z",
"/node_modules",
"/package-lock.json",
]
const rootPath = process.cwd()
const tmpPath = path.join(rootPath, ".tmp")
const buildPath = path.join(tmpPath, "build")
const bundlePath = path.join(tmpPath, "bundle.7z")
async function copySources(origin, to) {
const files = fs.readdirSync(origin)
if (!fs.existsSync(to)) {
await fs.promises.mkdir(to, { recursive: true })
}
for (const file of files) {
const filePath = path.join(origin, file)
// run a rexeg to check if the filePath is excluded
const isExcluded = excludedFiles.some((excludedPath) => {
return filePath.match(excludedPath)
})
if (isExcluded) {
continue
}
if (fs.lstatSync(filePath).isDirectory()) {
await copySources(filePath, path.join(to, file))
} else {
await fs.promises.copyFile(filePath, path.join(to, file))
}
}
}
async function createBundle(origin, desitinationFile) {
return new Promise((resolve, reject) => {
sevenzip.pack(origin, desitinationFile, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
async function main() {
if (!token) {
console.error("🛑 You need to pass a token as argument")
return
}
// create a .tmp folder
if (fs.existsSync(tmpPath)) {
await fs.promises.rm(tmpPath, { recursive: true, force: true })
}
try {
// try to read package.json
if (!fs.existsSync(path.resolve(rootPath, "package.json"))) {
console.error("🛑 package.json not found")
return
}
const packageJSON = require(path.resolve(rootPath, "package.json"))
// check if package.json has a main file
if (!packageJSON.main) {
console.error("🛑 package.json does not have a main file")
return
}
if (!fs.existsSync(path.resolve(rootPath, packageJSON.main))) {
console.error("🛑 main file not found")
return
}
console.log(packageJSON)
console.log("📦 Creating bundle...")
await copySources(rootPath, buildPath)
await createBundle(`${buildPath}/*`, bundlePath)
console.log("📦✅ Bundle created successfully")
console.log("🚚 Publishing bundle...")
const formData = new formdata()
formData.append("file", fs.createReadStream(bundlePath))
const response = await axios({
method: "PUT",
url: `${marketplaceAPIOrigin}/publish`,
headers: {
...formData.getHeaders(),
pkg: JSON.stringify(packageJSON),
Authorization: `Bearer ${token}`,
},
data: formData,
}).catch((error) => {
console.error("🛑 Error while publishing bundle \n\t", error.response?.data ?? error)
return false
})
if (response) {
console.log("🚚✅ Bundle published successfully! \n", response.data)
}
await fs.promises.rm(tmpPath, { recursive: true, force: true })
} catch (error) {
console.error("🛑 Error while publishing bundle \n\t", error)
await fs.promises.rm(tmpPath, { recursive: true, force: true })
}
}
main().catch(console.error)

2
vessel

@ -1 +1 @@
Subproject commit bdbd92a73cd8305e36fcf0b8d7ea0ceba8a1ffcc Subproject commit 829e1bce7bc7c1b613747eedfbcc21329c9c4442