feat: Implement core chat application UI, including chat, voice, members, DMs, and shared components.
Some checks failed
Build and Release / build-and-release (push) Failing after 0s

This commit is contained in:
Bryan1029384756
2026-02-14 01:57:15 -06:00
parent 6f12f98d30
commit 958cf56b23
51 changed files with 4761 additions and 1858 deletions

View File

@@ -33,7 +33,8 @@
"Bash(npm ls:*)", "Bash(npm ls:*)",
"Bash(git add:*)", "Bash(git add:*)",
"Bash(git commit:*)", "Bash(git commit:*)",
"Bash(git push:*)" "Bash(git push:*)",
"Bash(npm run build:web:*)"
] ]
} }
} }

21
TODO.md
View File

@@ -32,14 +32,25 @@
<!-- - Make people type passwords twice to make sure they dont mess up typing their password for registration. --> <!-- - Make people type passwords twice to make sure they dont mess up typing their password for registration. -->
<!-- How can we save user preferences for the app like individual user volumes, the position and size they have the floating stream popout, if they have categories collaped, the last channel they were in so we can open that channel when they open the app, etc. --> <!-- How can we save user preferences for the app like individual user volumes, the position and size they have the floating stream popout, if they have categories collaped, the last channel they were in so we can open that channel when they open the app, etc. -->
Can we make sure Voice and Video work. We have the users input and output devices but if i select any it dosent show it changed. I want to make sure that the users can select their input and output devices and that it works for livekit.
<!-- - Lets make it so we can upload a custom image for the server that will show on the sidebar. Make the image editing like how we do it for avatars but instead of a circle that we have to show users cut off its a square with a border radius, match it to the boarder radius of the server-item-wrapper --> <!-- - Lets make it so we can upload a custom image for the server that will show on the sidebar. Make the image editing like how we do it for avatars but instead of a circle that we have to show users cut off its a square with a border radius, match it to the boarder radius of the server-item-wrapper -->
- On mobile. lets redo the settings page to be more mobile friendly. I want it to look exactly the same on desktop but i need a little more mobile friendly for mobile.
What about the gitea workflow. Does that build the electron app still with the new setup? I dont need the android build right now just the same functionality as before with building for electron. <!-- - WHen i go idle and then come back sometimes it dosent show im online again, or maybe im idle and close the app and relaunch it, it says im idle still and i have to manualy change to online. -->
- Lets make a Popup menu on chat input to paste image or anything from clipboard. So its on option called "Paste". Should only popup if you right click the chat input box.
<!-- - If app is in the background or minimized it dosent show im online shows im offline -->
<!-- - When someone is sharing their screen and we are getting their audio lets ignore any sounds coming from our app. -->
<!-- - Custom intro sounds repeat when another user joins the chat even though the user with that custom sound didnt join and is already in voice chat.
- In the user options when you right click on a user in voice we should have the option to disconnect user from voice channel -->
<!-- - Can we Add volume slider for screenshare audio, and add a fullscreen button for screenshare, where we fullscreen the voice stage. -->
- Can we add a way to tell the user they are connecting to voice. Like show them its connecting so the user knows something is happening instead of them clicking on the voice stage again and again.

View File

@@ -1,7 +1,7 @@
{ {
"name": "@discord-clone/electron", "name": "@discord-clone/electron",
"private": true, "private": true,
"version": "1.0.14", "version": "1.0.16",
"description": "Discord Clone - Electron app", "description": "Discord Clone - Electron app",
"author": "Moyettes", "author": "Moyettes",
"type": "module", "type": "module",

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Discord Clone</title> <title>Discord Clone</title>
</head> </head>
<body> <body>

383
convex/auth.js Normal file
View File

@@ -0,0 +1,383 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateStatus = exports.getMyJoinSoundUrl = exports.updateProfile = exports.getPublicKeys = exports.createUserWithProfile = exports.verifyUser = exports.getSalt = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
var storageUrl_1 = require("./storageUrl");
function sha256Hex(input) {
return __awaiter(this, void 0, void 0, function () {
var buffer;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, crypto.subtle.digest("SHA-256", new TextEncoder().encode(input))];
case 1:
buffer = _a.sent();
return [2 /*return*/, Array.from(new Uint8Array(buffer))
.map(function (b) { return b.toString(16).padStart(2, "0"); })
.join("")];
}
});
});
}
// Get salt for a username (returns fake salt for non-existent users)
exports.getSalt = (0, server_1.query)({
args: { username: values_1.v.string() },
returns: values_1.v.object({ salt: values_1.v.string() }),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var user, fakeSalt;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("userProfiles")
.withIndex("by_username", function (q) { return q.eq("username", args.username); })
.unique()];
case 1:
user = _a.sent();
if (user) {
return [2 /*return*/, { salt: user.clientSalt }];
}
return [4 /*yield*/, sha256Hex("SERVER_SECRET_KEY" + args.username)];
case 2:
fakeSalt = _a.sent();
return [2 /*return*/, { salt: fakeSalt }];
}
});
}); },
});
// Verify user credentials (DAK comparison)
exports.verifyUser = (0, server_1.mutation)({
args: {
username: values_1.v.string(),
dak: values_1.v.string(),
},
returns: values_1.v.union(values_1.v.object({
success: values_1.v.boolean(),
userId: values_1.v.string(),
encryptedMK: values_1.v.string(),
encryptedPrivateKeys: values_1.v.string(),
publicKey: values_1.v.string(),
}), values_1.v.object({ error: values_1.v.string() })),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var user, hashedDAK;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("userProfiles")
.withIndex("by_username", function (q) { return q.eq("username", args.username); })
.unique()];
case 1:
user = _a.sent();
if (!user) {
return [2 /*return*/, { error: "Invalid credentials" }];
}
return [4 /*yield*/, sha256Hex(args.dak)];
case 2:
hashedDAK = _a.sent();
if (hashedDAK === user.hashedAuthKey) {
return [2 /*return*/, {
success: true,
userId: user._id,
encryptedMK: user.encryptedMasterKey,
encryptedPrivateKeys: user.encryptedPrivateKeys,
publicKey: user.publicIdentityKey,
}];
}
return [2 /*return*/, { error: "Invalid credentials" }];
}
});
}); },
});
// Register new user with crypto keys
exports.createUserWithProfile = (0, server_1.mutation)({
args: {
username: values_1.v.string(),
salt: values_1.v.string(),
encryptedMK: values_1.v.string(),
hak: values_1.v.string(),
publicKey: values_1.v.string(),
signingKey: values_1.v.string(),
encryptedPrivateKeys: values_1.v.string(),
inviteCode: values_1.v.optional(values_1.v.string()),
},
returns: values_1.v.union(values_1.v.object({ success: values_1.v.boolean(), userId: values_1.v.string() }), values_1.v.object({ error: values_1.v.string() })),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var existing, isFirstUser, invite, userId, everyoneRoleId, ownerRoleId, everyoneRole;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("userProfiles")
.withIndex("by_username", function (q) { return q.eq("username", args.username); })
.unique()];
case 1:
existing = _a.sent();
if (existing) {
return [2 /*return*/, { error: "Username taken" }];
}
return [4 /*yield*/, ctx.db.query("userProfiles").first()];
case 2:
isFirstUser = (_a.sent()) === null;
if (!!isFirstUser) return [3 /*break*/, 5];
if (!args.inviteCode) {
return [2 /*return*/, { error: "Invite code required" }];
}
return [4 /*yield*/, ctx.db
.query("invites")
.withIndex("by_code", function (q) { return q.eq("code", args.inviteCode); })
.unique()];
case 3:
invite = _a.sent();
if (!invite) {
return [2 /*return*/, { error: "Invalid invite code" }];
}
if (invite.expiresAt && Date.now() > invite.expiresAt) {
return [2 /*return*/, { error: "Invite expired" }];
}
if (invite.maxUses !== undefined &&
invite.maxUses !== null &&
invite.uses >= invite.maxUses) {
return [2 /*return*/, { error: "Invite max uses reached" }];
}
return [4 /*yield*/, ctx.db.patch(invite._id, { uses: invite.uses + 1 })];
case 4:
_a.sent();
_a.label = 5;
case 5: return [4 /*yield*/, ctx.db.insert("userProfiles", {
username: args.username,
clientSalt: args.salt,
encryptedMasterKey: args.encryptedMK,
hashedAuthKey: args.hak,
publicIdentityKey: args.publicKey,
publicSigningKey: args.signingKey,
encryptedPrivateKeys: args.encryptedPrivateKeys,
isAdmin: isFirstUser,
})];
case 6:
userId = _a.sent();
if (!isFirstUser) return [3 /*break*/, 11];
return [4 /*yield*/, ctx.db.insert("roles", {
name: "@everyone",
color: "#99aab5",
position: 0,
permissions: {
create_invite: true,
embed_links: true,
attach_files: true,
},
isHoist: false,
})];
case 7:
everyoneRoleId = _a.sent();
return [4 /*yield*/, ctx.db.insert("roles", {
name: "Owner",
color: "#e91e63",
position: 100,
permissions: {
manage_channels: true,
manage_roles: true,
manage_messages: true,
create_invite: true,
embed_links: true,
attach_files: true,
},
isHoist: true,
})];
case 8:
ownerRoleId = _a.sent();
return [4 /*yield*/, ctx.db.insert("userRoles", { userId: userId, roleId: everyoneRoleId })];
case 9:
_a.sent();
return [4 /*yield*/, ctx.db.insert("userRoles", { userId: userId, roleId: ownerRoleId })];
case 10:
_a.sent();
return [3 /*break*/, 14];
case 11: return [4 /*yield*/, ctx.db
.query("roles")
.filter(function (q) { return q.eq(q.field("name"), "@everyone"); })
.first()];
case 12:
everyoneRole = _a.sent();
if (!everyoneRole) return [3 /*break*/, 14];
return [4 /*yield*/, ctx.db.insert("userRoles", {
userId: userId,
roleId: everyoneRole._id,
})];
case 13:
_a.sent();
_a.label = 14;
case 14: return [2 /*return*/, { success: true, userId: userId }];
}
});
}); },
});
// Get all users' public keys
exports.getPublicKeys = (0, server_1.query)({
args: {},
returns: values_1.v.array(values_1.v.object({
id: values_1.v.string(),
username: values_1.v.string(),
public_identity_key: values_1.v.string(),
status: values_1.v.optional(values_1.v.string()),
displayName: values_1.v.optional(values_1.v.string()),
avatarUrl: values_1.v.optional(values_1.v.union(values_1.v.string(), values_1.v.null())),
aboutMe: values_1.v.optional(values_1.v.string()),
customStatus: values_1.v.optional(values_1.v.string()),
joinSoundUrl: values_1.v.optional(values_1.v.union(values_1.v.string(), values_1.v.null())),
})),
handler: function (ctx) { return __awaiter(void 0, void 0, void 0, function () {
var users, results, _i, users_1, u, avatarUrl, joinSoundUrl;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.query("userProfiles").collect()];
case 1:
users = _a.sent();
results = [];
_i = 0, users_1 = users;
_a.label = 2;
case 2:
if (!(_i < users_1.length)) return [3 /*break*/, 8];
u = users_1[_i];
avatarUrl = null;
if (!u.avatarStorageId) return [3 /*break*/, 4];
return [4 /*yield*/, (0, storageUrl_1.getPublicStorageUrl)(ctx, u.avatarStorageId)];
case 3:
avatarUrl = _a.sent();
_a.label = 4;
case 4:
joinSoundUrl = null;
if (!u.joinSoundStorageId) return [3 /*break*/, 6];
return [4 /*yield*/, (0, storageUrl_1.getPublicStorageUrl)(ctx, u.joinSoundStorageId)];
case 5:
joinSoundUrl = _a.sent();
_a.label = 6;
case 6:
results.push({
id: u._id,
username: u.username,
public_identity_key: u.publicIdentityKey,
status: u.status || "offline",
displayName: u.displayName,
avatarUrl: avatarUrl,
aboutMe: u.aboutMe,
customStatus: u.customStatus,
joinSoundUrl: joinSoundUrl,
});
_a.label = 7;
case 7:
_i++;
return [3 /*break*/, 2];
case 8: return [2 /*return*/, results];
}
});
}); },
});
// Update user profile (aboutMe, avatar, customStatus)
exports.updateProfile = (0, server_1.mutation)({
args: {
userId: values_1.v.id("userProfiles"),
displayName: values_1.v.optional(values_1.v.string()),
aboutMe: values_1.v.optional(values_1.v.string()),
avatarStorageId: values_1.v.optional(values_1.v.id("_storage")),
customStatus: values_1.v.optional(values_1.v.string()),
joinSoundStorageId: values_1.v.optional(values_1.v.id("_storage")),
removeJoinSound: values_1.v.optional(values_1.v.boolean()),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var patch;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
patch = {};
if (args.displayName !== undefined)
patch.displayName = args.displayName;
if (args.aboutMe !== undefined)
patch.aboutMe = args.aboutMe;
if (args.avatarStorageId !== undefined)
patch.avatarStorageId = args.avatarStorageId;
if (args.customStatus !== undefined)
patch.customStatus = args.customStatus;
if (args.joinSoundStorageId !== undefined)
patch.joinSoundStorageId = args.joinSoundStorageId;
if (args.removeJoinSound)
patch.joinSoundStorageId = undefined;
return [4 /*yield*/, ctx.db.patch(args.userId, patch)];
case 1:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});
// Get the current user's join sound URL
exports.getMyJoinSoundUrl = (0, server_1.query)({
args: { userId: values_1.v.id("userProfiles") },
returns: values_1.v.union(values_1.v.string(), values_1.v.null()),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var user;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.get(args.userId)];
case 1:
user = _a.sent();
if (!(user === null || user === void 0 ? void 0 : user.joinSoundStorageId))
return [2 /*return*/, null];
return [4 /*yield*/, (0, storageUrl_1.getPublicStorageUrl)(ctx, user.joinSoundStorageId)];
case 2: return [2 /*return*/, _a.sent()];
}
});
}); },
});
// Update user status
exports.updateStatus = (0, server_1.mutation)({
args: {
userId: values_1.v.id("userProfiles"),
status: values_1.v.string(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.patch(args.userId, { status: args.status })];
case 1:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});

187
convex/categories.js Normal file
View File

@@ -0,0 +1,187 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.reorder = exports.remove = exports.rename = exports.create = exports.list = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
// List all categories ordered by position
exports.list = (0, server_1.query)({
args: {},
returns: values_1.v.array(values_1.v.object({
_id: values_1.v.id("categories"),
_creationTime: values_1.v.number(),
name: values_1.v.string(),
position: values_1.v.number(),
})),
handler: function (ctx) { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("categories")
.withIndex("by_position")
.collect()];
case 1: return [2 /*return*/, _a.sent()];
}
});
}); },
});
// Create a new category
exports.create = (0, server_1.mutation)({
args: { name: values_1.v.string() },
returns: values_1.v.object({ id: values_1.v.id("categories") }),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var name, all, maxPos, id;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
name = args.name.trim();
if (!name)
throw new Error("Category name required");
return [4 /*yield*/, ctx.db.query("categories").collect()];
case 1:
all = _a.sent();
maxPos = all.reduce(function (max, c) { return Math.max(max, c.position); }, -1000);
return [4 /*yield*/, ctx.db.insert("categories", {
name: name,
position: maxPos + 1000,
})];
case 2:
id = _a.sent();
return [2 /*return*/, { id: id }];
}
});
}); },
});
// Rename a category
exports.rename = (0, server_1.mutation)({
args: {
id: values_1.v.id("categories"),
name: values_1.v.string(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var name, cat;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
name = args.name.trim();
if (!name)
throw new Error("Category name required");
return [4 /*yield*/, ctx.db.get(args.id)];
case 1:
cat = _a.sent();
if (!cat)
throw new Error("Category not found");
return [4 /*yield*/, ctx.db.patch(args.id, { name: name })];
case 2:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});
// Delete a category (moves its channels to uncategorized)
exports.remove = (0, server_1.mutation)({
args: { id: values_1.v.id("categories") },
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var cat, channels, _i, channels_1, ch;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.get(args.id)];
case 1:
cat = _a.sent();
if (!cat)
throw new Error("Category not found");
return [4 /*yield*/, ctx.db
.query("channels")
.withIndex("by_category", function (q) { return q.eq("categoryId", args.id); })
.collect()];
case 2:
channels = _a.sent();
_i = 0, channels_1 = channels;
_a.label = 3;
case 3:
if (!(_i < channels_1.length)) return [3 /*break*/, 6];
ch = channels_1[_i];
return [4 /*yield*/, ctx.db.patch(ch._id, { categoryId: undefined })];
case 4:
_a.sent();
_a.label = 5;
case 5:
_i++;
return [3 /*break*/, 3];
case 6: return [4 /*yield*/, ctx.db.delete(args.id)];
case 7:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});
// Batch reorder categories
exports.reorder = (0, server_1.mutation)({
args: {
updates: values_1.v.array(values_1.v.object({
id: values_1.v.id("categories"),
position: values_1.v.number(),
})),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var _i, _a, u;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_i = 0, _a = args.updates;
_b.label = 1;
case 1:
if (!(_i < _a.length)) return [3 /*break*/, 4];
u = _a[_i];
return [4 /*yield*/, ctx.db.patch(u.id, { position: u.position })];
case 2:
_b.sent();
_b.label = 3;
case 3:
_i++;
return [3 /*break*/, 1];
case 4: return [2 /*return*/, null];
}
});
}); },
});

138
convex/channelKeys.js Normal file
View File

@@ -0,0 +1,138 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getKeysForUser = exports.uploadKeys = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
// Batch upsert encrypted key bundles
exports.uploadKeys = (0, server_1.mutation)({
args: {
keys: values_1.v.array(values_1.v.object({
channelId: values_1.v.id("channels"),
userId: values_1.v.id("userProfiles"),
encryptedKeyBundle: values_1.v.string(),
keyVersion: values_1.v.number(),
})),
},
returns: values_1.v.object({ success: values_1.v.boolean(), count: values_1.v.number() }),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var _loop_1, _i, _a, keyData;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_loop_1 = function (keyData) {
var existing;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
if (!keyData.channelId || !keyData.userId || !keyData.encryptedKeyBundle) {
return [2 /*return*/, "continue"];
}
return [4 /*yield*/, ctx.db
.query("channelKeys")
.withIndex("by_channel_and_user", function (q) {
return q.eq("channelId", keyData.channelId).eq("userId", keyData.userId);
})
.unique()];
case 1:
existing = _c.sent();
if (!existing) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.patch(existing._id, {
encryptedKeyBundle: keyData.encryptedKeyBundle,
keyVersion: keyData.keyVersion,
})];
case 2:
_c.sent();
return [3 /*break*/, 5];
case 3: return [4 /*yield*/, ctx.db.insert("channelKeys", {
channelId: keyData.channelId,
userId: keyData.userId,
encryptedKeyBundle: keyData.encryptedKeyBundle,
keyVersion: keyData.keyVersion,
})];
case 4:
_c.sent();
_c.label = 5;
case 5: return [2 /*return*/];
}
});
};
_i = 0, _a = args.keys;
_b.label = 1;
case 1:
if (!(_i < _a.length)) return [3 /*break*/, 4];
keyData = _a[_i];
return [5 /*yield**/, _loop_1(keyData)];
case 2:
_b.sent();
_b.label = 3;
case 3:
_i++;
return [3 /*break*/, 1];
case 4: return [2 /*return*/, { success: true, count: args.keys.length }];
}
});
}); },
});
// Get user's encrypted key bundles (reactive!)
exports.getKeysForUser = (0, server_1.query)({
args: { userId: values_1.v.id("userProfiles") },
returns: values_1.v.array(values_1.v.object({
channel_id: values_1.v.id("channels"),
encrypted_key_bundle: values_1.v.string(),
key_version: values_1.v.number(),
})),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var keys;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("channelKeys")
.withIndex("by_user", function (q) { return q.eq("userId", args.userId); })
.collect()];
case 1:
keys = _a.sent();
return [2 /*return*/, keys.map(function (k) { return ({
channel_id: k.channelId,
encrypted_key_bundle: k.encryptedKeyBundle,
key_version: k.keyVersion,
}); })];
}
});
}); },
});

390
convex/channels.js Normal file
View File

@@ -0,0 +1,390 @@
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.remove = exports.reorderChannels = exports.moveChannel = exports.rename = exports.updateTopic = exports.create = exports.get = exports.list = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
var api_1 = require("./_generated/api");
function deleteByChannel(ctx, table, channelId) {
return __awaiter(this, void 0, void 0, function () {
var docs, _i, docs_1, doc;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.query(table)
.withIndex("by_channel", function (q) { return q.eq("channelId", channelId); })
.collect()];
case 1:
docs = _a.sent();
_i = 0, docs_1 = docs;
_a.label = 2;
case 2:
if (!(_i < docs_1.length)) return [3 /*break*/, 5];
doc = docs_1[_i];
return [4 /*yield*/, ctx.db.delete(doc._id)];
case 3:
_a.sent();
_a.label = 4;
case 4:
_i++;
return [3 /*break*/, 2];
case 5: return [2 /*return*/];
}
});
});
}
// List all non-DM channels
exports.list = (0, server_1.query)({
args: {},
returns: values_1.v.array(values_1.v.object({
_id: values_1.v.id("channels"),
_creationTime: values_1.v.number(),
name: values_1.v.string(),
type: values_1.v.string(),
categoryId: values_1.v.optional(values_1.v.id("categories")),
topic: values_1.v.optional(values_1.v.string()),
position: values_1.v.optional(values_1.v.number()),
})),
handler: function (ctx) { return __awaiter(void 0, void 0, void 0, function () {
var channels;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.query("channels").collect()];
case 1:
channels = _a.sent();
return [2 /*return*/, channels
.filter(function (c) { return c.type !== "dm"; })
.sort(function (a, b) { var _a, _b; return ((_a = a.position) !== null && _a !== void 0 ? _a : 0) - ((_b = b.position) !== null && _b !== void 0 ? _b : 0) || a.name.localeCompare(b.name); })];
}
});
}); },
});
// Get single channel by ID
exports.get = (0, server_1.query)({
args: { id: values_1.v.id("channels") },
returns: values_1.v.union(values_1.v.object({
_id: values_1.v.id("channels"),
_creationTime: values_1.v.number(),
name: values_1.v.string(),
type: values_1.v.string(),
categoryId: values_1.v.optional(values_1.v.id("categories")),
topic: values_1.v.optional(values_1.v.string()),
position: values_1.v.optional(values_1.v.number()),
}), values_1.v.null()),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.get(args.id)];
case 1: return [2 /*return*/, _a.sent()];
}
});
}); },
});
// Create new channel
exports.create = (0, server_1.mutation)({
args: {
name: values_1.v.string(),
type: values_1.v.optional(values_1.v.string()),
categoryId: values_1.v.optional(values_1.v.id("categories")),
topic: values_1.v.optional(values_1.v.string()),
position: values_1.v.optional(values_1.v.number()),
},
returns: values_1.v.object({ id: values_1.v.id("channels") }),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var existing, position, allChannels, sameCategory, maxPos, id;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!args.name.trim()) {
throw new Error("Channel name required");
}
return [4 /*yield*/, ctx.db
.query("channels")
.withIndex("by_name", function (q) { return q.eq("name", args.name); })
.unique()];
case 1:
existing = _a.sent();
if (existing) {
throw new Error("Channel already exists");
}
position = args.position;
if (!(position === undefined)) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.query("channels").collect()];
case 2:
allChannels = _a.sent();
sameCategory = allChannels.filter(function (c) { return c.categoryId === args.categoryId && c.type !== "dm"; });
maxPos = sameCategory.reduce(function (max, c) { var _a; return Math.max(max, (_a = c.position) !== null && _a !== void 0 ? _a : 0); }, -1000);
position = maxPos + 1000;
_a.label = 3;
case 3: return [4 /*yield*/, ctx.db.insert("channels", {
name: args.name,
type: args.type || "text",
categoryId: args.categoryId,
topic: args.topic,
position: position,
})];
case 4:
id = _a.sent();
return [2 /*return*/, { id: id }];
}
});
}); },
});
// Update channel topic
exports.updateTopic = (0, server_1.mutation)({
args: {
id: values_1.v.id("channels"),
topic: values_1.v.string(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var channel;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.get(args.id)];
case 1:
channel = _a.sent();
if (!channel)
throw new Error("Channel not found");
return [4 /*yield*/, ctx.db.patch(args.id, { topic: args.topic })];
case 2:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});
// Rename channel
exports.rename = (0, server_1.mutation)({
args: {
id: values_1.v.id("channels"),
name: values_1.v.string(),
},
returns: values_1.v.object({
_id: values_1.v.id("channels"),
_creationTime: values_1.v.number(),
name: values_1.v.string(),
type: values_1.v.string(),
categoryId: values_1.v.optional(values_1.v.id("categories")),
topic: values_1.v.optional(values_1.v.string()),
position: values_1.v.optional(values_1.v.number()),
}),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var channel;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!args.name.trim()) {
throw new Error("Name required");
}
return [4 /*yield*/, ctx.db.get(args.id)];
case 1:
channel = _a.sent();
if (!channel) {
throw new Error("Channel not found");
}
return [4 /*yield*/, ctx.db.patch(args.id, { name: args.name })];
case 2:
_a.sent();
return [2 /*return*/, __assign(__assign({}, channel), { name: args.name })];
}
});
}); },
});
// Move a channel to a different category with a new position
exports.moveChannel = (0, server_1.mutation)({
args: {
id: values_1.v.id("channels"),
categoryId: values_1.v.optional(values_1.v.id("categories")),
position: values_1.v.number(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var channel;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.get(args.id)];
case 1:
channel = _a.sent();
if (!channel)
throw new Error("Channel not found");
return [4 /*yield*/, ctx.db.patch(args.id, {
categoryId: args.categoryId,
position: args.position,
})];
case 2:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});
// Batch reorder channels
exports.reorderChannels = (0, server_1.mutation)({
args: {
updates: values_1.v.array(values_1.v.object({
id: values_1.v.id("channels"),
categoryId: values_1.v.optional(values_1.v.id("categories")),
position: values_1.v.number(),
})),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var _i, _a, u;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_i = 0, _a = args.updates;
_b.label = 1;
case 1:
if (!(_i < _a.length)) return [3 /*break*/, 4];
u = _a[_i];
return [4 /*yield*/, ctx.db.patch(u.id, {
categoryId: u.categoryId,
position: u.position,
})];
case 2:
_b.sent();
_b.label = 3;
case 3:
_i++;
return [3 /*break*/, 1];
case 4: return [2 /*return*/, null];
}
});
}); },
});
// Delete channel + cascade messages and keys
exports.remove = (0, server_1.mutation)({
args: { id: values_1.v.id("channels") },
returns: values_1.v.object({ success: values_1.v.boolean() }),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var channel, messages, _loop_1, _i, messages_1, msg;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.get(args.id)];
case 1:
channel = _a.sent();
if (!channel) {
throw new Error("Channel not found");
}
return [4 /*yield*/, ctx.db
.query("messages")
.withIndex("by_channel", function (q) { return q.eq("channelId", args.id); })
.collect()];
case 2:
messages = _a.sent();
_loop_1 = function (msg) {
var reactions, _b, reactions_1, r;
return __generator(this, function (_c) {
switch (_c.label) {
case 0: return [4 /*yield*/, ctx.db
.query("messageReactions")
.withIndex("by_message", function (q) { return q.eq("messageId", msg._id); })
.collect()];
case 1:
reactions = _c.sent();
_b = 0, reactions_1 = reactions;
_c.label = 2;
case 2:
if (!(_b < reactions_1.length)) return [3 /*break*/, 5];
r = reactions_1[_b];
return [4 /*yield*/, ctx.db.delete(r._id)];
case 3:
_c.sent();
_c.label = 4;
case 4:
_b++;
return [3 /*break*/, 2];
case 5: return [4 /*yield*/, ctx.db.delete(msg._id)];
case 6:
_c.sent();
return [2 /*return*/];
}
});
};
_i = 0, messages_1 = messages;
_a.label = 3;
case 3:
if (!(_i < messages_1.length)) return [3 /*break*/, 6];
msg = messages_1[_i];
return [5 /*yield**/, _loop_1(msg)];
case 4:
_a.sent();
_a.label = 5;
case 5:
_i++;
return [3 /*break*/, 3];
case 6: return [4 /*yield*/, deleteByChannel(ctx, "channelKeys", args.id)];
case 7:
_a.sent();
return [4 /*yield*/, deleteByChannel(ctx, "dmParticipants", args.id)];
case 8:
_a.sent();
return [4 /*yield*/, deleteByChannel(ctx, "typingIndicators", args.id)];
case 9:
_a.sent();
return [4 /*yield*/, deleteByChannel(ctx, "voiceStates", args.id)];
case 10:
_a.sent();
return [4 /*yield*/, deleteByChannel(ctx, "channelReadState", args.id)];
case 11:
_a.sent();
// Clear AFK setting if this channel was the AFK channel
return [4 /*yield*/, ctx.runMutation(api_1.internal.serverSettings.clearAfkChannel, { channelId: args.id })];
case 12:
// Clear AFK setting if this channel was the AFK channel
_a.sent();
return [4 /*yield*/, ctx.db.delete(args.id)];
case 13:
_a.sent();
return [2 /*return*/, { success: true }];
}
});
}); },
});

7
convex/convex.config.js Normal file
View File

@@ -0,0 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var server_1 = require("convex/server");
var convex_config_js_1 = require("@convex-dev/presence/convex.config.js");
var app = (0, server_1.defineApp)();
app.use(convex_config_js_1.default);
exports.default = app;

158
convex/dms.js Normal file
View File

@@ -0,0 +1,158 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.listDMs = exports.openDM = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
var storageUrl_1 = require("./storageUrl");
exports.openDM = (0, server_1.mutation)({
args: {
userId: values_1.v.id("userProfiles"),
targetUserId: values_1.v.id("userProfiles"),
},
returns: values_1.v.object({
channelId: values_1.v.id("channels"),
created: values_1.v.boolean(),
}),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var sorted, dmName, existing, channelId;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (args.userId === args.targetUserId) {
throw new Error("Cannot DM yourself");
}
sorted = [args.userId, args.targetUserId].sort();
dmName = "dm-".concat(sorted[0], "-").concat(sorted[1]);
return [4 /*yield*/, ctx.db
.query("channels")
.withIndex("by_name", function (q) { return q.eq("name", dmName); })
.unique()];
case 1:
existing = _a.sent();
if (existing) {
return [2 /*return*/, { channelId: existing._id, created: false }];
}
return [4 /*yield*/, ctx.db.insert("channels", {
name: dmName,
type: "dm",
})];
case 2:
channelId = _a.sent();
return [4 /*yield*/, Promise.all([
ctx.db.insert("dmParticipants", { channelId: channelId, userId: args.userId }),
ctx.db.insert("dmParticipants", { channelId: channelId, userId: args.targetUserId }),
])];
case 3:
_a.sent();
return [2 /*return*/, { channelId: channelId, created: true }];
}
});
}); },
});
exports.listDMs = (0, server_1.query)({
args: { userId: values_1.v.id("userProfiles") },
returns: values_1.v.array(values_1.v.object({
channel_id: values_1.v.id("channels"),
channel_name: values_1.v.string(),
other_user_id: values_1.v.string(),
other_username: values_1.v.string(),
other_user_status: values_1.v.optional(values_1.v.string()),
other_user_avatar_url: values_1.v.optional(values_1.v.union(values_1.v.string(), values_1.v.null())),
})),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var myParticipations, results;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("dmParticipants")
.withIndex("by_user", function (q) { return q.eq("userId", args.userId); })
.collect()];
case 1:
myParticipations = _a.sent();
return [4 /*yield*/, Promise.all(myParticipations.map(function (part) { return __awaiter(void 0, void 0, void 0, function () {
var channel, otherParts, otherPart, otherUser, avatarUrl, _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, ctx.db.get(part.channelId)];
case 1:
channel = _b.sent();
if (!channel || channel.type !== "dm")
return [2 /*return*/, null];
return [4 /*yield*/, ctx.db
.query("dmParticipants")
.withIndex("by_channel", function (q) { return q.eq("channelId", part.channelId); })
.collect()];
case 2:
otherParts = _b.sent();
otherPart = otherParts.find(function (p) { return p.userId !== args.userId; });
if (!otherPart)
return [2 /*return*/, null];
return [4 /*yield*/, ctx.db.get(otherPart.userId)];
case 3:
otherUser = _b.sent();
if (!otherUser)
return [2 /*return*/, null];
if (!otherUser.avatarStorageId) return [3 /*break*/, 5];
return [4 /*yield*/, (0, storageUrl_1.getPublicStorageUrl)(ctx, otherUser.avatarStorageId)];
case 4:
_a = _b.sent();
return [3 /*break*/, 6];
case 5:
_a = null;
_b.label = 6;
case 6:
avatarUrl = _a;
return [2 /*return*/, {
channel_id: part.channelId,
channel_name: channel.name,
other_user_id: otherUser._id,
other_username: otherUser.username,
other_user_status: otherUser.status || "offline",
other_user_avatar_url: avatarUrl,
}];
}
});
}); }))];
case 2:
results = _a.sent();
return [2 /*return*/, results.filter(function (r) { return r !== null; })];
}
});
}); },
});

71
convex/files.js Normal file
View File

@@ -0,0 +1,71 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getFileUrl = exports.generateUploadUrl = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
var storageUrl_1 = require("./storageUrl");
// Generate upload URL for client-side uploads
exports.generateUploadUrl = (0, server_1.mutation)({
args: {},
returns: values_1.v.string(),
handler: function (ctx) { return __awaiter(void 0, void 0, void 0, function () {
var url;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.storage.generateUploadUrl()];
case 1:
url = _a.sent();
return [2 /*return*/, (0, storageUrl_1.rewriteToPublicUrl)(url)];
}
});
}); },
});
// Get file URL from storage ID
exports.getFileUrl = (0, server_1.query)({
args: { storageId: values_1.v.id("_storage") },
returns: values_1.v.union(values_1.v.string(), values_1.v.null()),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, (0, storageUrl_1.getPublicStorageUrl)(ctx, args.storageId)];
case 1: return [2 /*return*/, _a.sent()];
}
});
}); },
});

86
convex/gifs.js Normal file
View File

@@ -0,0 +1,86 @@
"use strict";
"use node";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.categories = exports.search = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
// Search GIFs via Tenor API
exports.search = (0, server_1.action)({
args: {
q: values_1.v.string(),
limit: values_1.v.optional(values_1.v.number()),
},
returns: values_1.v.any(),
handler: function (_ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var apiKey, limit, url, response;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
apiKey = process.env.TENOR_API_KEY;
if (!apiKey) {
console.warn("TENOR_API_KEY missing");
return [2 /*return*/, { results: [] }];
}
limit = args.limit || 8;
url = "https://tenor.googleapis.com/v2/search?q=".concat(encodeURIComponent(args.q), "&key=").concat(apiKey, "&limit=").concat(limit);
return [4 /*yield*/, fetch(url)];
case 1:
response = _a.sent();
if (!response.ok) {
console.error("Tenor API Error:", response.statusText);
return [2 /*return*/, { results: [] }];
}
return [4 /*yield*/, response.json()];
case 2: return [2 /*return*/, _a.sent()];
}
});
}); },
});
// Get GIF categories
exports.categories = (0, server_1.action)({
args: {},
returns: values_1.v.any(),
handler: function () { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
// Return static categories (same as the JSON file in backend)
// These are loaded from the frontend data file
return [2 /*return*/, { categories: [] }];
});
}); },
});

131
convex/invites.js Normal file
View File

@@ -0,0 +1,131 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.revoke = exports.use = exports.create = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
// Create invite with encrypted payload
exports.create = (0, server_1.mutation)({
args: {
code: values_1.v.string(),
encryptedPayload: values_1.v.string(),
createdBy: values_1.v.id("userProfiles"),
maxUses: values_1.v.optional(values_1.v.number()),
expiresAt: values_1.v.optional(values_1.v.number()),
keyVersion: values_1.v.number(),
},
returns: values_1.v.object({ success: values_1.v.boolean() }),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.insert("invites", {
code: args.code,
encryptedPayload: args.encryptedPayload,
createdBy: args.createdBy,
maxUses: args.maxUses,
uses: 0,
expiresAt: args.expiresAt,
keyVersion: args.keyVersion,
})];
case 1:
_a.sent();
return [2 /*return*/, { success: true }];
}
});
}); },
});
// Fetch and validate invite (returns encrypted payload)
exports.use = (0, server_1.query)({
args: { code: values_1.v.string() },
returns: values_1.v.union(values_1.v.object({
encryptedPayload: values_1.v.string(),
keyVersion: values_1.v.number(),
}), values_1.v.object({ error: values_1.v.string() })),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var invite;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("invites")
.withIndex("by_code", function (q) { return q.eq("code", args.code); })
.unique()];
case 1:
invite = _a.sent();
if (!invite) {
return [2 /*return*/, { error: "Invite not found" }];
}
if (invite.expiresAt && Date.now() > invite.expiresAt) {
return [2 /*return*/, { error: "Invite expired" }];
}
if (invite.maxUses !== undefined &&
invite.maxUses !== null &&
invite.uses >= invite.maxUses) {
return [2 /*return*/, { error: "Invite max uses reached" }];
}
return [2 /*return*/, {
encryptedPayload: invite.encryptedPayload,
keyVersion: invite.keyVersion,
}];
}
});
}); },
});
// Revoke invite
exports.revoke = (0, server_1.mutation)({
args: { code: values_1.v.string() },
returns: values_1.v.object({ success: values_1.v.boolean() }),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var invite;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("invites")
.withIndex("by_code", function (q) { return q.eq("code", args.code); })
.unique()];
case 1:
invite = _a.sent();
if (!invite) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.delete(invite._id)];
case 2:
_a.sent();
_a.label = 3;
case 3: return [2 /*return*/, { success: true }];
}
});
}); },
});

139
convex/members.js Normal file
View File

@@ -0,0 +1,139 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getChannelMembers = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
var storageUrl_1 = require("./storageUrl");
exports.getChannelMembers = (0, server_1.query)({
args: {
channelId: values_1.v.id("channels"),
},
returns: values_1.v.any(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var channelKeyDocs, seenUsers, members, _loop_1, _i, channelKeyDocs_1, doc;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("channelKeys")
.withIndex("by_channel", function (q) { return q.eq("channelId", args.channelId); })
.collect()];
case 1:
channelKeyDocs = _a.sent();
seenUsers = new Set();
members = [];
_loop_1 = function (doc) {
var odId, user, userRoleDocs, roles, _b, userRoleDocs_1, ur, role, avatarUrl;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
odId = doc.userId.toString();
if (seenUsers.has(odId))
return [2 /*return*/, "continue"];
seenUsers.add(odId);
return [4 /*yield*/, ctx.db.get(doc.userId)];
case 1:
user = _c.sent();
if (!user)
return [2 /*return*/, "continue"];
return [4 /*yield*/, ctx.db
.query("userRoles")
.withIndex("by_user", function (q) { return q.eq("userId", doc.userId); })
.collect()];
case 2:
userRoleDocs = _c.sent();
roles = [];
_b = 0, userRoleDocs_1 = userRoleDocs;
_c.label = 3;
case 3:
if (!(_b < userRoleDocs_1.length)) return [3 /*break*/, 6];
ur = userRoleDocs_1[_b];
return [4 /*yield*/, ctx.db.get(ur.roleId)];
case 4:
role = _c.sent();
if (role) {
roles.push({
id: role._id,
name: role.name,
color: role.color,
position: role.position,
isHoist: role.isHoist,
});
}
_c.label = 5;
case 5:
_b++;
return [3 /*break*/, 3];
case 6:
avatarUrl = null;
if (!user.avatarStorageId) return [3 /*break*/, 8];
return [4 /*yield*/, (0, storageUrl_1.getPublicStorageUrl)(ctx, user.avatarStorageId)];
case 7:
avatarUrl = _c.sent();
_c.label = 8;
case 8:
members.push({
id: user._id,
username: user.username,
status: user.status || "offline",
roles: roles.sort(function (a, b) { return b.position - a.position; }),
avatarUrl: avatarUrl,
aboutMe: user.aboutMe,
customStatus: user.customStatus,
});
return [2 /*return*/];
}
});
};
_i = 0, channelKeyDocs_1 = channelKeyDocs;
_a.label = 2;
case 2:
if (!(_i < channelKeyDocs_1.length)) return [3 /*break*/, 5];
doc = channelKeyDocs_1[_i];
return [5 /*yield**/, _loop_1(doc)];
case 3:
_a.sent();
_a.label = 4;
case 4:
_i++;
return [3 /*break*/, 2];
case 5: return [2 /*return*/, members];
}
});
}); },
});

300
convex/messages.js Normal file
View File

@@ -0,0 +1,300 @@
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.remove = exports.listPinned = exports.pin = exports.edit = exports.send = exports.list = void 0;
var server_1 = require("./_generated/server");
var server_2 = require("convex/server");
var values_1 = require("convex/values");
var storageUrl_1 = require("./storageUrl");
var roles_1 = require("./roles");
function enrichMessage(ctx, msg, userId) {
return __awaiter(this, void 0, void 0, function () {
var sender, avatarUrl, reactionDocs, reactions, _i, reactionDocs_1, r, entry, replyToUsername, replyToContent, replyToNonce, replyToAvatarUrl, repliedMsg, repliedSender;
var _a;
var _b;
return __generator(this, function (_c) {
switch (_c.label) {
case 0: return [4 /*yield*/, ctx.db.get(msg.senderId)];
case 1:
sender = _c.sent();
avatarUrl = null;
if (!(sender === null || sender === void 0 ? void 0 : sender.avatarStorageId)) return [3 /*break*/, 3];
return [4 /*yield*/, (0, storageUrl_1.getPublicStorageUrl)(ctx, sender.avatarStorageId)];
case 2:
avatarUrl = _c.sent();
_c.label = 3;
case 3: return [4 /*yield*/, ctx.db
.query("messageReactions")
.withIndex("by_message", function (q) { return q.eq("messageId", msg._id); })
.collect()];
case 4:
reactionDocs = _c.sent();
reactions = {};
for (_i = 0, reactionDocs_1 = reactionDocs; _i < reactionDocs_1.length; _i++) {
r = reactionDocs_1[_i];
entry = ((_a = reactions[_b = r.emoji]) !== null && _a !== void 0 ? _a : (reactions[_b] = { count: 0, me: false }));
entry.count++;
if (userId && r.userId === userId) {
entry.me = true;
}
}
replyToUsername = null;
replyToContent = null;
replyToNonce = null;
replyToAvatarUrl = null;
if (!msg.replyTo) return [3 /*break*/, 8];
return [4 /*yield*/, ctx.db.get(msg.replyTo)];
case 5:
repliedMsg = _c.sent();
if (!repliedMsg) return [3 /*break*/, 8];
return [4 /*yield*/, ctx.db.get(repliedMsg.senderId)];
case 6:
repliedSender = _c.sent();
replyToUsername = (repliedSender === null || repliedSender === void 0 ? void 0 : repliedSender.username) || "Unknown";
replyToContent = repliedMsg.ciphertext;
replyToNonce = repliedMsg.nonce;
if (!(repliedSender === null || repliedSender === void 0 ? void 0 : repliedSender.avatarStorageId)) return [3 /*break*/, 8];
return [4 /*yield*/, (0, storageUrl_1.getPublicStorageUrl)(ctx, repliedSender.avatarStorageId)];
case 7:
replyToAvatarUrl = _c.sent();
_c.label = 8;
case 8: return [2 /*return*/, {
id: msg._id,
channel_id: msg.channelId,
sender_id: msg.senderId,
ciphertext: msg.ciphertext,
nonce: msg.nonce,
signature: msg.signature,
key_version: msg.keyVersion,
created_at: new Date(msg._creationTime).toISOString(),
username: (sender === null || sender === void 0 ? void 0 : sender.username) || "Unknown",
public_signing_key: (sender === null || sender === void 0 ? void 0 : sender.publicSigningKey) || "",
avatarUrl: avatarUrl,
reactions: Object.keys(reactions).length > 0 ? reactions : null,
replyToId: msg.replyTo || null,
replyToUsername: replyToUsername,
replyToContent: replyToContent,
replyToNonce: replyToNonce,
replyToAvatarUrl: replyToAvatarUrl,
editedAt: msg.editedAt || null,
pinned: msg.pinned || false,
}];
}
});
});
}
exports.list = (0, server_1.query)({
args: {
paginationOpts: server_2.paginationOptsValidator,
channelId: values_1.v.id("channels"),
userId: values_1.v.optional(values_1.v.id("userProfiles")),
},
returns: values_1.v.any(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var result, enrichedPage;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("messages")
.withIndex("by_channel", function (q) { return q.eq("channelId", args.channelId); })
.order("desc")
.paginate(args.paginationOpts)];
case 1:
result = _a.sent();
return [4 /*yield*/, Promise.all(result.page.map(function (msg) { return enrichMessage(ctx, msg, args.userId); }))];
case 2:
enrichedPage = _a.sent();
return [2 /*return*/, __assign(__assign({}, result), { page: enrichedPage })];
}
});
}); },
});
exports.send = (0, server_1.mutation)({
args: {
channelId: values_1.v.id("channels"),
senderId: values_1.v.id("userProfiles"),
ciphertext: values_1.v.string(),
nonce: values_1.v.string(),
signature: values_1.v.string(),
keyVersion: values_1.v.number(),
replyTo: values_1.v.optional(values_1.v.id("messages")),
},
returns: values_1.v.object({ id: values_1.v.id("messages") }),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var id;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.insert("messages", {
channelId: args.channelId,
senderId: args.senderId,
ciphertext: args.ciphertext,
nonce: args.nonce,
signature: args.signature,
keyVersion: args.keyVersion,
replyTo: args.replyTo,
})];
case 1:
id = _a.sent();
return [2 /*return*/, { id: id }];
}
});
}); },
});
exports.edit = (0, server_1.mutation)({
args: {
id: values_1.v.id("messages"),
ciphertext: values_1.v.string(),
nonce: values_1.v.string(),
signature: values_1.v.string(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.patch(args.id, {
ciphertext: args.ciphertext,
nonce: args.nonce,
signature: args.signature,
editedAt: Date.now(),
})];
case 1:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});
exports.pin = (0, server_1.mutation)({
args: {
id: values_1.v.id("messages"),
pinned: values_1.v.boolean(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.patch(args.id, { pinned: args.pinned })];
case 1:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});
exports.listPinned = (0, server_1.query)({
args: {
channelId: values_1.v.id("channels"),
userId: values_1.v.optional(values_1.v.id("userProfiles")),
},
returns: values_1.v.any(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var pinned;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("messages")
.withIndex("by_channel_pinned", function (q) {
return q.eq("channelId", args.channelId).eq("pinned", true);
})
.collect()];
case 1:
pinned = _a.sent();
return [2 /*return*/, Promise.all(pinned.map(function (msg) { return enrichMessage(ctx, msg, args.userId); }))];
}
});
}); },
});
exports.remove = (0, server_1.mutation)({
args: { id: values_1.v.id("messages"), userId: values_1.v.id("userProfiles") },
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var message, isSender, roles, canManage, reactions, _i, reactions_1, r;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.get(args.id)];
case 1:
message = _a.sent();
if (!message)
throw new Error("Message not found");
isSender = message.senderId === args.userId;
if (!!isSender) return [3 /*break*/, 3];
return [4 /*yield*/, (0, roles_1.getRolesForUser)(ctx, args.userId)];
case 2:
roles = _a.sent();
canManage = roles.some(function (role) { var _a; return (_a = role.permissions) === null || _a === void 0 ? void 0 : _a.manage_messages; });
if (!canManage) {
throw new Error("Not authorized to delete this message");
}
_a.label = 3;
case 3: return [4 /*yield*/, ctx.db
.query("messageReactions")
.withIndex("by_message", function (q) { return q.eq("messageId", args.id); })
.collect()];
case 4:
reactions = _a.sent();
_i = 0, reactions_1 = reactions;
_a.label = 5;
case 5:
if (!(_i < reactions_1.length)) return [3 /*break*/, 8];
r = reactions_1[_i];
return [4 /*yield*/, ctx.db.delete(r._id)];
case 6:
_a.sent();
_a.label = 7;
case 7:
_i++;
return [3 /*break*/, 5];
case 8: return [4 /*yield*/, ctx.db.delete(args.id)];
case 9:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});

85
convex/presence.js Normal file
View File

@@ -0,0 +1,85 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.disconnect = exports.list = exports.heartbeat = void 0;
var server_1 = require("./_generated/server");
var api_1 = require("./_generated/api");
var values_1 = require("convex/values");
var presence_1 = require("@convex-dev/presence");
var presence = new presence_1.Presence(api_1.components.presence);
exports.heartbeat = (0, server_1.mutation)({
args: {
roomId: values_1.v.string(),
userId: values_1.v.string(),
sessionId: values_1.v.string(),
interval: values_1.v.number(),
},
handler: function (ctx_1, _a) { return __awaiter(void 0, [ctx_1, _a], void 0, function (ctx, _b) {
var roomId = _b.roomId, userId = _b.userId, sessionId = _b.sessionId, interval = _b.interval;
return __generator(this, function (_c) {
switch (_c.label) {
case 0: return [4 /*yield*/, presence.heartbeat(ctx, roomId, userId, sessionId, interval)];
case 1: return [2 /*return*/, _c.sent()];
}
});
}); },
});
exports.list = (0, server_1.query)({
args: { roomToken: values_1.v.string() },
handler: function (ctx_1, _a) { return __awaiter(void 0, [ctx_1, _a], void 0, function (ctx, _b) {
var roomToken = _b.roomToken;
return __generator(this, function (_c) {
switch (_c.label) {
case 0: return [4 /*yield*/, presence.list(ctx, roomToken)];
case 1: return [2 /*return*/, _c.sent()];
}
});
}); },
});
exports.disconnect = (0, server_1.mutation)({
args: { sessionToken: values_1.v.string() },
handler: function (ctx_1, _a) { return __awaiter(void 0, [ctx_1, _a], void 0, function (ctx, _b) {
var sessionToken = _b.sessionToken;
return __generator(this, function (_c) {
switch (_c.label) {
case 0: return [4 /*yield*/, presence.disconnect(ctx, sessionToken)];
case 1: return [2 /*return*/, _c.sent()];
}
});
}); },
});

111
convex/reactions.js Normal file
View File

@@ -0,0 +1,111 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.remove = exports.add = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
// Add reaction (upsert - no duplicates)
exports.add = (0, server_1.mutation)({
args: {
messageId: values_1.v.id("messages"),
userId: values_1.v.id("userProfiles"),
emoji: values_1.v.string(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var existing;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("messageReactions")
.withIndex("by_message_user_emoji", function (q) {
return q
.eq("messageId", args.messageId)
.eq("userId", args.userId)
.eq("emoji", args.emoji);
})
.unique()];
case 1:
existing = _a.sent();
if (!!existing) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.insert("messageReactions", {
messageId: args.messageId,
userId: args.userId,
emoji: args.emoji,
})];
case 2:
_a.sent();
_a.label = 3;
case 3: return [2 /*return*/, null];
}
});
}); },
});
// Remove reaction
exports.remove = (0, server_1.mutation)({
args: {
messageId: values_1.v.id("messages"),
userId: values_1.v.id("userProfiles"),
emoji: values_1.v.string(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var existing;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("messageReactions")
.withIndex("by_message_user_emoji", function (q) {
return q
.eq("messageId", args.messageId)
.eq("userId", args.userId)
.eq("emoji", args.emoji);
})
.unique()];
case 1:
existing = _a.sent();
if (!existing) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.delete(existing._id)];
case 2:
_a.sent();
_a.label = 3;
case 3: return [2 /*return*/, null];
}
});
}); },
});

191
convex/readState.js Normal file
View File

@@ -0,0 +1,191 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getLatestMessageTimestamps = exports.getAllReadStates = exports.markRead = exports.getReadState = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
// Get read state for a single channel
exports.getReadState = (0, server_1.query)({
args: {
userId: values_1.v.id("userProfiles"),
channelId: values_1.v.id("channels"),
},
returns: values_1.v.union(values_1.v.object({
lastReadTimestamp: values_1.v.number(),
}), values_1.v.null()),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var state;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("channelReadState")
.withIndex("by_user_and_channel", function (q) {
return q.eq("userId", args.userId).eq("channelId", args.channelId);
})
.unique()];
case 1:
state = _a.sent();
if (!state)
return [2 /*return*/, null];
return [2 /*return*/, { lastReadTimestamp: state.lastReadTimestamp }];
}
});
}); },
});
// Mark a channel as read up to a given timestamp
exports.markRead = (0, server_1.mutation)({
args: {
userId: values_1.v.id("userProfiles"),
channelId: values_1.v.id("channels"),
lastReadTimestamp: values_1.v.number(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var existing;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("channelReadState")
.withIndex("by_user_and_channel", function (q) {
return q.eq("userId", args.userId).eq("channelId", args.channelId);
})
.unique()];
case 1:
existing = _a.sent();
if (!existing) return [3 /*break*/, 4];
if (!(args.lastReadTimestamp > existing.lastReadTimestamp)) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.patch(existing._id, {
lastReadTimestamp: args.lastReadTimestamp,
})];
case 2:
_a.sent();
_a.label = 3;
case 3: return [3 /*break*/, 6];
case 4: return [4 /*yield*/, ctx.db.insert("channelReadState", {
userId: args.userId,
channelId: args.channelId,
lastReadTimestamp: args.lastReadTimestamp,
})];
case 5:
_a.sent();
_a.label = 6;
case 6: return [2 /*return*/, null];
}
});
}); },
});
// Get all read states for a user (used by Sidebar)
exports.getAllReadStates = (0, server_1.query)({
args: {
userId: values_1.v.id("userProfiles"),
},
returns: values_1.v.array(values_1.v.object({
channelId: values_1.v.id("channels"),
lastReadTimestamp: values_1.v.number(),
})),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var states;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("channelReadState")
.withIndex("by_user", function (q) { return q.eq("userId", args.userId); })
.collect()];
case 1:
states = _a.sent();
return [2 /*return*/, states.map(function (s) { return ({
channelId: s.channelId,
lastReadTimestamp: s.lastReadTimestamp,
}); })];
}
});
}); },
});
// Get the latest message timestamp per channel (used by Sidebar)
exports.getLatestMessageTimestamps = (0, server_1.query)({
args: {
channelIds: values_1.v.array(values_1.v.id("channels")),
},
returns: values_1.v.array(values_1.v.object({
channelId: values_1.v.id("channels"),
latestTimestamp: values_1.v.number(),
})),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var results, _loop_1, _i, _a, channelId;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
results = [];
_loop_1 = function (channelId) {
var latestMsg;
return __generator(this, function (_c) {
switch (_c.label) {
case 0: return [4 /*yield*/, ctx.db
.query("messages")
.withIndex("by_channel", function (q) { return q.eq("channelId", channelId); })
.order("desc")
.first()];
case 1:
latestMsg = _c.sent();
if (latestMsg) {
results.push({
channelId: channelId,
latestTimestamp: Math.floor(latestMsg._creationTime),
});
}
return [2 /*return*/];
}
});
};
_i = 0, _a = args.channelIds;
_b.label = 1;
case 1:
if (!(_i < _a.length)) return [3 /*break*/, 4];
channelId = _a[_i];
return [5 /*yield**/, _loop_1(channelId)];
case 2:
_b.sent();
_b.label = 3;
case 3:
_i++;
return [3 /*break*/, 1];
case 4: return [2 /*return*/, results];
}
});
}); },
});

330
convex/roles.js Normal file
View File

@@ -0,0 +1,330 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMyPermissions = exports.unassign = exports.assign = exports.listMembers = exports.remove = exports.update = exports.create = exports.list = void 0;
exports.getRolesForUser = getRolesForUser;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
var PERMISSION_KEYS = [
"manage_channels",
"manage_roles",
"manage_messages",
"create_invite",
"embed_links",
"attach_files",
"move_members",
"mute_members",
];
function getRolesForUser(ctx, userId) {
return __awaiter(this, void 0, void 0, function () {
var assignments, roles;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("userRoles")
.withIndex("by_user", function (q) { return q.eq("userId", userId); })
.collect()];
case 1:
assignments = _a.sent();
return [4 /*yield*/, Promise.all(assignments.map(function (ur) { return ctx.db.get(ur.roleId); }))];
case 2:
roles = _a.sent();
return [2 /*return*/, roles.filter(function (r) { return r !== null; })];
}
});
});
}
// List all roles
exports.list = (0, server_1.query)({
args: {},
returns: values_1.v.array(values_1.v.any()),
handler: function (ctx) { return __awaiter(void 0, void 0, void 0, function () {
var roles;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.query("roles").collect()];
case 1:
roles = _a.sent();
return [2 /*return*/, roles.sort(function (a, b) { return (b.position || 0) - (a.position || 0); })];
}
});
}); },
});
// Create new role
exports.create = (0, server_1.mutation)({
args: {
name: values_1.v.optional(values_1.v.string()),
color: values_1.v.optional(values_1.v.string()),
permissions: values_1.v.optional(values_1.v.any()),
position: values_1.v.optional(values_1.v.number()),
isHoist: values_1.v.optional(values_1.v.boolean()),
},
returns: values_1.v.any(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var id;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.insert("roles", {
name: args.name || "new role",
color: args.color || "#99aab5",
position: args.position || 0,
permissions: args.permissions || {},
isHoist: args.isHoist || false,
})];
case 1:
id = _a.sent();
return [4 /*yield*/, ctx.db.get(id)];
case 2: return [2 /*return*/, _a.sent()];
}
});
}); },
});
// Update role properties
exports.update = (0, server_1.mutation)({
args: {
id: values_1.v.id("roles"),
name: values_1.v.optional(values_1.v.string()),
color: values_1.v.optional(values_1.v.string()),
permissions: values_1.v.optional(values_1.v.any()),
position: values_1.v.optional(values_1.v.number()),
isHoist: values_1.v.optional(values_1.v.boolean()),
},
returns: values_1.v.any(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var role, id, fields, updates, _i, _a, _b, key, value;
return __generator(this, function (_c) {
switch (_c.label) {
case 0: return [4 /*yield*/, ctx.db.get(args.id)];
case 1:
role = _c.sent();
if (!role)
throw new Error("Role not found");
id = args.id, fields = __rest(args, ["id"]);
updates = {};
for (_i = 0, _a = Object.entries(fields); _i < _a.length; _i++) {
_b = _a[_i], key = _b[0], value = _b[1];
if (value !== undefined)
updates[key] = value;
}
if (!(Object.keys(updates).length > 0)) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.patch(id, updates)];
case 2:
_c.sent();
_c.label = 3;
case 3: return [4 /*yield*/, ctx.db.get(id)];
case 4: return [2 /*return*/, _c.sent()];
}
});
}); },
});
// Delete role
exports.remove = (0, server_1.mutation)({
args: { id: values_1.v.id("roles") },
returns: values_1.v.object({ success: values_1.v.boolean() }),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var role, assignments, _i, assignments_1, a;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.get(args.id)];
case 1:
role = _a.sent();
if (!role)
throw new Error("Role not found");
return [4 /*yield*/, ctx.db
.query("userRoles")
.withIndex("by_role", function (q) { return q.eq("roleId", args.id); })
.collect()];
case 2:
assignments = _a.sent();
_i = 0, assignments_1 = assignments;
_a.label = 3;
case 3:
if (!(_i < assignments_1.length)) return [3 /*break*/, 6];
a = assignments_1[_i];
return [4 /*yield*/, ctx.db.delete(a._id)];
case 4:
_a.sent();
_a.label = 5;
case 5:
_i++;
return [3 /*break*/, 3];
case 6: return [4 /*yield*/, ctx.db.delete(args.id)];
case 7:
_a.sent();
return [2 /*return*/, { success: true }];
}
});
}); },
});
// List members with roles
exports.listMembers = (0, server_1.query)({
args: {},
returns: values_1.v.array(values_1.v.any()),
handler: function (ctx) { return __awaiter(void 0, void 0, void 0, function () {
var users;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.query("userProfiles").collect()];
case 1:
users = _a.sent();
return [4 /*yield*/, Promise.all(users.map(function (user) { return __awaiter(void 0, void 0, void 0, function () {
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_a = {
id: user._id,
username: user.username,
public_identity_key: user.publicIdentityKey
};
return [4 /*yield*/, getRolesForUser(ctx, user._id)];
case 1: return [2 /*return*/, (_a.roles = _b.sent(),
_a)];
}
});
}); }))];
case 2: return [2 /*return*/, _a.sent()];
}
});
}); },
});
// Assign role to user
exports.assign = (0, server_1.mutation)({
args: {
roleId: values_1.v.id("roles"),
userId: values_1.v.id("userProfiles"),
},
returns: values_1.v.object({ success: values_1.v.boolean() }),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var existing;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("userRoles")
.withIndex("by_user_and_role", function (q) {
return q.eq("userId", args.userId).eq("roleId", args.roleId);
})
.unique()];
case 1:
existing = _a.sent();
if (!!existing) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.insert("userRoles", {
userId: args.userId,
roleId: args.roleId,
})];
case 2:
_a.sent();
_a.label = 3;
case 3: return [2 /*return*/, { success: true }];
}
});
}); },
});
// Remove role from user
exports.unassign = (0, server_1.mutation)({
args: {
roleId: values_1.v.id("roles"),
userId: values_1.v.id("userProfiles"),
},
returns: values_1.v.object({ success: values_1.v.boolean() }),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var existing;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("userRoles")
.withIndex("by_user_and_role", function (q) {
return q.eq("userId", args.userId).eq("roleId", args.roleId);
})
.unique()];
case 1:
existing = _a.sent();
if (!existing) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.delete(existing._id)];
case 2:
_a.sent();
_a.label = 3;
case 3: return [2 /*return*/, { success: true }];
}
});
}); },
});
// Get current user's aggregated permissions
exports.getMyPermissions = (0, server_1.query)({
args: { userId: values_1.v.id("userProfiles") },
returns: values_1.v.object({
manage_channels: values_1.v.boolean(),
manage_roles: values_1.v.boolean(),
manage_messages: values_1.v.boolean(),
create_invite: values_1.v.boolean(),
embed_links: values_1.v.boolean(),
attach_files: values_1.v.boolean(),
move_members: values_1.v.boolean(),
mute_members: values_1.v.boolean(),
}),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var roles, finalPerms, _loop_1, _i, PERMISSION_KEYS_1, key;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, getRolesForUser(ctx, args.userId)];
case 1:
roles = _a.sent();
finalPerms = {};
_loop_1 = function (key) {
finalPerms[key] = roles.some(function (role) { var _a; return (_a = role.permissions) === null || _a === void 0 ? void 0 : _a[key]; });
};
for (_i = 0, PERMISSION_KEYS_1 = PERMISSION_KEYS; _i < PERMISSION_KEYS_1.length; _i++) {
key = PERMISSION_KEYS_1[_i];
_loop_1(key);
}
return [2 /*return*/, finalPerms];
}
});
}); },
});

123
convex/schema.js Normal file
View File

@@ -0,0 +1,123 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var server_1 = require("convex/server");
var values_1 = require("convex/values");
exports.default = (0, server_1.defineSchema)({
userProfiles: (0, server_1.defineTable)({
username: values_1.v.string(),
clientSalt: values_1.v.string(),
encryptedMasterKey: values_1.v.string(),
hashedAuthKey: values_1.v.string(),
publicIdentityKey: values_1.v.string(),
publicSigningKey: values_1.v.string(),
encryptedPrivateKeys: values_1.v.string(),
isAdmin: values_1.v.boolean(),
status: values_1.v.optional(values_1.v.string()),
displayName: values_1.v.optional(values_1.v.string()),
avatarStorageId: values_1.v.optional(values_1.v.id("_storage")),
aboutMe: values_1.v.optional(values_1.v.string()),
customStatus: values_1.v.optional(values_1.v.string()),
joinSoundStorageId: values_1.v.optional(values_1.v.id("_storage")),
}).index("by_username", ["username"]),
categories: (0, server_1.defineTable)({
name: values_1.v.string(),
position: values_1.v.number(),
}).index("by_position", ["position"]),
channels: (0, server_1.defineTable)({
name: values_1.v.string(),
type: values_1.v.string(), // 'text' | 'voice' | 'dm'
categoryId: values_1.v.optional(values_1.v.id("categories")),
topic: values_1.v.optional(values_1.v.string()),
position: values_1.v.optional(values_1.v.number()),
}).index("by_name", ["name"])
.index("by_category", ["categoryId"]),
messages: (0, server_1.defineTable)({
channelId: values_1.v.id("channels"),
senderId: values_1.v.id("userProfiles"),
ciphertext: values_1.v.string(),
nonce: values_1.v.string(),
signature: values_1.v.string(),
keyVersion: values_1.v.number(),
replyTo: values_1.v.optional(values_1.v.id("messages")),
editedAt: values_1.v.optional(values_1.v.number()),
pinned: values_1.v.optional(values_1.v.boolean()),
}).index("by_channel", ["channelId"])
.index("by_channel_pinned", ["channelId", "pinned"]),
messageReactions: (0, server_1.defineTable)({
messageId: values_1.v.id("messages"),
userId: values_1.v.id("userProfiles"),
emoji: values_1.v.string(),
})
.index("by_message", ["messageId"])
.index("by_message_user_emoji", ["messageId", "userId", "emoji"]),
channelKeys: (0, server_1.defineTable)({
channelId: values_1.v.id("channels"),
userId: values_1.v.id("userProfiles"),
encryptedKeyBundle: values_1.v.string(),
keyVersion: values_1.v.number(),
})
.index("by_channel", ["channelId"])
.index("by_user", ["userId"])
.index("by_channel_and_user", ["channelId", "userId"]),
roles: (0, server_1.defineTable)({
name: values_1.v.string(),
color: values_1.v.string(),
position: values_1.v.number(),
permissions: values_1.v.any(), // JSON object of permissions
isHoist: values_1.v.boolean(),
}),
userRoles: (0, server_1.defineTable)({
userId: values_1.v.id("userProfiles"),
roleId: values_1.v.id("roles"),
})
.index("by_user", ["userId"])
.index("by_role", ["roleId"])
.index("by_user_and_role", ["userId", "roleId"]),
invites: (0, server_1.defineTable)({
code: values_1.v.string(),
encryptedPayload: values_1.v.string(),
createdBy: values_1.v.id("userProfiles"),
maxUses: values_1.v.optional(values_1.v.number()),
uses: values_1.v.number(),
expiresAt: values_1.v.optional(values_1.v.number()), // timestamp
keyVersion: values_1.v.number(),
}).index("by_code", ["code"]),
dmParticipants: (0, server_1.defineTable)({
channelId: values_1.v.id("channels"),
userId: values_1.v.id("userProfiles"),
})
.index("by_channel", ["channelId"])
.index("by_user", ["userId"]),
typingIndicators: (0, server_1.defineTable)({
channelId: values_1.v.id("channels"),
userId: values_1.v.id("userProfiles"),
username: values_1.v.string(),
expiresAt: values_1.v.number(), // timestamp
}).index("by_channel", ["channelId"]),
voiceStates: (0, server_1.defineTable)({
channelId: values_1.v.id("channels"),
userId: values_1.v.id("userProfiles"),
username: values_1.v.string(),
isMuted: values_1.v.boolean(),
isDeafened: values_1.v.boolean(),
isScreenSharing: values_1.v.boolean(),
isServerMuted: values_1.v.boolean(),
watchingStream: values_1.v.optional(values_1.v.id("userProfiles")),
})
.index("by_channel", ["channelId"])
.index("by_user", ["userId"]),
channelReadState: (0, server_1.defineTable)({
userId: values_1.v.id("userProfiles"),
channelId: values_1.v.id("channels"),
lastReadTimestamp: values_1.v.number(),
})
.index("by_user", ["userId"])
.index("by_channel", ["channelId"])
.index("by_user_and_channel", ["userId", "channelId"]),
serverSettings: (0, server_1.defineTable)({
serverName: values_1.v.optional(values_1.v.string()),
afkChannelId: values_1.v.optional(values_1.v.id("channels")),
afkTimeout: values_1.v.number(), // seconds (default 300 = 5 min)
iconStorageId: values_1.v.optional(values_1.v.id("_storage")),
}),
});

230
convex/serverSettings.js Normal file
View File

@@ -0,0 +1,230 @@
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.clearAfkChannel = exports.updateIcon = exports.updateName = exports.update = exports.get = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
var roles_1 = require("./roles");
exports.get = (0, server_1.query)({
args: {},
returns: values_1.v.any(),
handler: function (ctx) { return __awaiter(void 0, void 0, void 0, function () {
var settings, iconUrl;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.query("serverSettings").first()];
case 1:
settings = _a.sent();
if (!settings)
return [2 /*return*/, null];
iconUrl = null;
if (!settings.iconStorageId) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.storage.getUrl(settings.iconStorageId)];
case 2:
iconUrl = _a.sent();
_a.label = 3;
case 3: return [2 /*return*/, __assign(__assign({}, settings), { iconUrl: iconUrl })];
}
});
}); },
});
exports.update = (0, server_1.mutation)({
args: {
userId: values_1.v.id("userProfiles"),
afkChannelId: values_1.v.optional(values_1.v.id("channels")),
afkTimeout: values_1.v.number(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var roles, canManage, channel, existing;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, (0, roles_1.getRolesForUser)(ctx, args.userId)];
case 1:
roles = _a.sent();
canManage = roles.some(function (role) { var _a; return (_a = role.permissions) === null || _a === void 0 ? void 0 : _a["manage_channels"]; });
if (!canManage) {
throw new Error("You don't have permission to manage server settings");
}
// Validate timeout range
if (args.afkTimeout < 60 || args.afkTimeout > 3600) {
throw new Error("AFK timeout must be between 60 and 3600 seconds");
}
if (!args.afkChannelId) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.get(args.afkChannelId)];
case 2:
channel = _a.sent();
if (!channel)
throw new Error("AFK channel not found");
if (channel.type !== "voice")
throw new Error("AFK channel must be a voice channel");
_a.label = 3;
case 3: return [4 /*yield*/, ctx.db.query("serverSettings").first()];
case 4:
existing = _a.sent();
if (!existing) return [3 /*break*/, 6];
return [4 /*yield*/, ctx.db.patch(existing._id, {
afkChannelId: args.afkChannelId,
afkTimeout: args.afkTimeout,
})];
case 5:
_a.sent();
return [3 /*break*/, 8];
case 6: return [4 /*yield*/, ctx.db.insert("serverSettings", {
afkChannelId: args.afkChannelId,
afkTimeout: args.afkTimeout,
})];
case 7:
_a.sent();
_a.label = 8;
case 8: return [2 /*return*/, null];
}
});
}); },
});
exports.updateName = (0, server_1.mutation)({
args: {
userId: values_1.v.id("userProfiles"),
serverName: values_1.v.string(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var roles, canManage, name, existing;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, (0, roles_1.getRolesForUser)(ctx, args.userId)];
case 1:
roles = _a.sent();
canManage = roles.some(function (role) { var _a; return (_a = role.permissions) === null || _a === void 0 ? void 0 : _a["manage_channels"]; });
if (!canManage) {
throw new Error("You don't have permission to manage server settings");
}
name = args.serverName.trim();
if (name.length === 0 || name.length > 100) {
throw new Error("Server name must be between 1 and 100 characters");
}
return [4 /*yield*/, ctx.db.query("serverSettings").first()];
case 2:
existing = _a.sent();
if (!existing) return [3 /*break*/, 4];
return [4 /*yield*/, ctx.db.patch(existing._id, { serverName: name })];
case 3:
_a.sent();
return [3 /*break*/, 6];
case 4: return [4 /*yield*/, ctx.db.insert("serverSettings", {
serverName: name,
afkTimeout: 300,
})];
case 5:
_a.sent();
_a.label = 6;
case 6: return [2 /*return*/, null];
}
});
}); },
});
exports.updateIcon = (0, server_1.mutation)({
args: {
userId: values_1.v.id("userProfiles"),
iconStorageId: values_1.v.optional(values_1.v.id("_storage")),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var roles, canManage, existing;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, (0, roles_1.getRolesForUser)(ctx, args.userId)];
case 1:
roles = _a.sent();
canManage = roles.some(function (role) { var _a; return (_a = role.permissions) === null || _a === void 0 ? void 0 : _a["manage_channels"]; });
if (!canManage) {
throw new Error("You don't have permission to manage server settings");
}
return [4 /*yield*/, ctx.db.query("serverSettings").first()];
case 2:
existing = _a.sent();
if (!existing) return [3 /*break*/, 4];
return [4 /*yield*/, ctx.db.patch(existing._id, {
iconStorageId: args.iconStorageId,
})];
case 3:
_a.sent();
return [3 /*break*/, 6];
case 4: return [4 /*yield*/, ctx.db.insert("serverSettings", {
iconStorageId: args.iconStorageId,
afkTimeout: 300,
})];
case 5:
_a.sent();
_a.label = 6;
case 6: return [2 /*return*/, null];
}
});
}); },
});
exports.clearAfkChannel = (0, server_1.internalMutation)({
args: { channelId: values_1.v.id("channels") },
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var settings;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.query("serverSettings").first()];
case 1:
settings = _a.sent();
if (!(settings && settings.afkChannelId === args.channelId)) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.patch(settings._id, { afkChannelId: undefined })];
case 2:
_a.sent();
_a.label = 3;
case 3: return [2 /*return*/, null];
}
});
}); },
});

72
convex/storageUrl.js Normal file
View File

@@ -0,0 +1,72 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.rewriteToPublicUrl = rewriteToPublicUrl;
exports.getPublicStorageUrl = getPublicStorageUrl;
// Change this if your public IP changes
var PUBLIC_CONVEX_URL = "http://72.26.56.3:3210";
/** Rewrite any URL to use the public hostname/port/protocol */
function rewriteToPublicUrl(url) {
try {
var original = new URL(url);
var target = new URL(PUBLIC_CONVEX_URL);
original.hostname = target.hostname;
original.port = target.port;
original.protocol = target.protocol;
return original.toString();
}
catch (_a) {
return url;
}
}
/** Get a storage file URL rewritten to the public address */
function getPublicStorageUrl(ctx, storageId) {
return __awaiter(this, void 0, void 0, function () {
var url;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.storage.getUrl(storageId)];
case 1:
url = _a.sent();
if (!url)
return [2 /*return*/, null];
return [2 /*return*/, rewriteToPublicUrl(url)];
}
});
});
}

167
convex/typing.js Normal file
View File

@@ -0,0 +1,167 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.cleanExpired = exports.getTyping = exports.stopTyping = exports.startTyping = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
var api_1 = require("./_generated/api");
var TYPING_TTL_MS = 6000;
exports.startTyping = (0, server_1.mutation)({
args: {
channelId: values_1.v.id("channels"),
userId: values_1.v.id("userProfiles"),
username: values_1.v.string(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var expiresAt, existing, userTyping;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
expiresAt = Date.now() + TYPING_TTL_MS;
return [4 /*yield*/, ctx.db
.query("typingIndicators")
.withIndex("by_channel", function (q) { return q.eq("channelId", args.channelId); })
.collect()];
case 1:
existing = _a.sent();
userTyping = existing.find(function (t) { return t.userId === args.userId; });
if (!userTyping) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.patch(userTyping._id, { expiresAt: expiresAt })];
case 2:
_a.sent();
return [3 /*break*/, 5];
case 3: return [4 /*yield*/, ctx.db.insert("typingIndicators", {
channelId: args.channelId,
userId: args.userId,
username: args.username,
expiresAt: expiresAt,
})];
case 4:
_a.sent();
_a.label = 5;
case 5: return [4 /*yield*/, ctx.scheduler.runAfter(TYPING_TTL_MS, api_1.internal.typing.cleanExpired, {})];
case 6:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});
exports.stopTyping = (0, server_1.mutation)({
args: {
channelId: values_1.v.id("channels"),
userId: values_1.v.id("userProfiles"),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var indicators, mine;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("typingIndicators")
.withIndex("by_channel", function (q) { return q.eq("channelId", args.channelId); })
.collect()];
case 1:
indicators = _a.sent();
mine = indicators.find(function (t) { return t.userId === args.userId; });
if (!mine) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.delete(mine._id)];
case 2:
_a.sent();
_a.label = 3;
case 3: return [2 /*return*/, null];
}
});
}); },
});
exports.getTyping = (0, server_1.query)({
args: { channelId: values_1.v.id("channels") },
returns: values_1.v.array(values_1.v.object({
userId: values_1.v.id("userProfiles"),
username: values_1.v.string(),
})),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var now, indicators;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
now = Date.now();
return [4 /*yield*/, ctx.db
.query("typingIndicators")
.withIndex("by_channel", function (q) { return q.eq("channelId", args.channelId); })
.collect()];
case 1:
indicators = _a.sent();
return [2 /*return*/, indicators
.filter(function (t) { return t.expiresAt > now; })
.map(function (t) { return ({ userId: t.userId, username: t.username }); })];
}
});
}); },
});
exports.cleanExpired = (0, server_1.internalMutation)({
args: {},
returns: values_1.v.null(),
handler: function (ctx) { return __awaiter(void 0, void 0, void 0, function () {
var now, expired, _i, expired_1, t;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
now = Date.now();
return [4 /*yield*/, ctx.db.query("typingIndicators").collect()];
case 1:
expired = _a.sent();
_i = 0, expired_1 = expired;
_a.label = 2;
case 2:
if (!(_i < expired_1.length)) return [3 /*break*/, 5];
t = expired_1[_i];
if (!(t.expiresAt <= now)) return [3 /*break*/, 4];
return [4 /*yield*/, ctx.db.delete(t._id)];
case 3:
_a.sent();
_a.label = 4;
case 4:
_i++;
return [3 /*break*/, 2];
case 5: return [2 /*return*/, null];
}
});
}); },
});

76
convex/voice.js Normal file
View File

@@ -0,0 +1,76 @@
"use strict";
"use node";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getToken = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
var livekit_server_sdk_1 = require("livekit-server-sdk");
// Generate LiveKit token for voice channel
exports.getToken = (0, server_1.action)({
args: {
channelId: values_1.v.string(),
userId: values_1.v.string(),
username: values_1.v.string(),
},
returns: values_1.v.object({ token: values_1.v.string() }),
handler: function (_ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var apiKey, apiSecret, at, token;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
apiKey = process.env.LIVEKIT_API_KEY || "devkey";
apiSecret = process.env.LIVEKIT_API_SECRET || "secret";
at = new livekit_server_sdk_1.AccessToken(apiKey, apiSecret, {
identity: args.userId,
name: args.username,
});
at.addGrant({
roomJoin: true,
room: args.channelId,
canPublish: true,
canSubscribe: true,
});
return [4 /*yield*/, at.toJwt()];
case 1:
token = _a.sent();
return [2 /*return*/, { token: token }];
}
});
}); },
});

456
convex/voiceState.js Normal file
View File

@@ -0,0 +1,456 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.moveUser = exports.disconnectUser = exports.afkMove = exports.getAll = exports.setWatchingStream = exports.serverMute = exports.updateState = exports.leave = exports.join = void 0;
var server_1 = require("./_generated/server");
var values_1 = require("convex/values");
var storageUrl_1 = require("./storageUrl");
var roles_1 = require("./roles");
function removeUserVoiceStates(ctx, userId) {
return __awaiter(this, void 0, void 0, function () {
var existing, _i, existing_1, vs;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("voiceStates")
.withIndex("by_user", function (q) { return q.eq("userId", userId); })
.collect()];
case 1:
existing = _a.sent();
_i = 0, existing_1 = existing;
_a.label = 2;
case 2:
if (!(_i < existing_1.length)) return [3 /*break*/, 5];
vs = existing_1[_i];
return [4 /*yield*/, ctx.db.delete(vs._id)];
case 3:
_a.sent();
_a.label = 4;
case 4:
_i++;
return [3 /*break*/, 2];
case 5: return [2 /*return*/];
}
});
});
}
exports.join = (0, server_1.mutation)({
args: {
channelId: values_1.v.id("channels"),
userId: values_1.v.id("userProfiles"),
username: values_1.v.string(),
isMuted: values_1.v.boolean(),
isDeafened: values_1.v.boolean(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, removeUserVoiceStates(ctx, args.userId)];
case 1:
_a.sent();
return [4 /*yield*/, ctx.db.insert("voiceStates", {
channelId: args.channelId,
userId: args.userId,
username: args.username,
isMuted: args.isMuted,
isDeafened: args.isDeafened,
isScreenSharing: false,
isServerMuted: false,
})];
case 2:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});
exports.leave = (0, server_1.mutation)({
args: {
userId: values_1.v.id("userProfiles"),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, removeUserVoiceStates(ctx, args.userId)];
case 1:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});
exports.updateState = (0, server_1.mutation)({
args: {
userId: values_1.v.id("userProfiles"),
isMuted: values_1.v.optional(values_1.v.boolean()),
isDeafened: values_1.v.optional(values_1.v.boolean()),
isScreenSharing: values_1.v.optional(values_1.v.boolean()),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var existing, _1, updates, filtered, allStates, _i, allStates_1, s;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db
.query("voiceStates")
.withIndex("by_user", function (q) { return q.eq("userId", args.userId); })
.first()];
case 1:
existing = _a.sent();
if (!existing) return [3 /*break*/, 7];
_1 = args.userId, updates = __rest(args, ["userId"]);
filtered = Object.fromEntries(Object.entries(updates).filter(function (_a) {
var val = _a[1];
return val !== undefined;
}));
return [4 /*yield*/, ctx.db.patch(existing._id, filtered)];
case 2:
_a.sent();
if (!(args.isScreenSharing === false)) return [3 /*break*/, 7];
return [4 /*yield*/, ctx.db.query("voiceStates").collect()];
case 3:
allStates = _a.sent();
_i = 0, allStates_1 = allStates;
_a.label = 4;
case 4:
if (!(_i < allStates_1.length)) return [3 /*break*/, 7];
s = allStates_1[_i];
if (!(s.watchingStream === args.userId)) return [3 /*break*/, 6];
return [4 /*yield*/, ctx.db.patch(s._id, { watchingStream: undefined })];
case 5:
_a.sent();
_a.label = 6;
case 6:
_i++;
return [3 /*break*/, 4];
case 7: return [2 /*return*/, null];
}
});
}); },
});
exports.serverMute = (0, server_1.mutation)({
args: {
actorUserId: values_1.v.id("userProfiles"),
targetUserId: values_1.v.id("userProfiles"),
isServerMuted: values_1.v.boolean(),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var roles, canMute, existing;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, (0, roles_1.getRolesForUser)(ctx, args.actorUserId)];
case 1:
roles = _a.sent();
canMute = roles.some(function (role) { var _a; return (_a = role.permissions) === null || _a === void 0 ? void 0 : _a["mute_members"]; });
if (!canMute) {
throw new Error("You don't have permission to server mute members");
}
return [4 /*yield*/, ctx.db
.query("voiceStates")
.withIndex("by_user", function (q) { return q.eq("userId", args.targetUserId); })
.first()];
case 2:
existing = _a.sent();
if (!existing)
throw new Error("Target user is not in a voice channel");
return [4 /*yield*/, ctx.db.patch(existing._id, { isServerMuted: args.isServerMuted })];
case 3:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});
exports.setWatchingStream = (0, server_1.mutation)({
args: {
userId: values_1.v.id("userProfiles"),
watchingStream: values_1.v.optional(values_1.v.id("userProfiles")),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var existing;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0: return [4 /*yield*/, ctx.db
.query("voiceStates")
.withIndex("by_user", function (q) { return q.eq("userId", args.userId); })
.first()];
case 1:
existing = _b.sent();
if (!existing) return [3 /*break*/, 3];
return [4 /*yield*/, ctx.db.patch(existing._id, {
watchingStream: (_a = args.watchingStream) !== null && _a !== void 0 ? _a : undefined,
})];
case 2:
_b.sent();
_b.label = 3;
case 3: return [2 /*return*/, null];
}
});
}); },
});
exports.getAll = (0, server_1.query)({
args: {},
returns: values_1.v.any(),
handler: function (ctx) { return __awaiter(void 0, void 0, void 0, function () {
var states, grouped, _i, states_1, s, user, avatarUrl, joinSoundUrl;
var _a, _b;
var _c;
return __generator(this, function (_d) {
switch (_d.label) {
case 0: return [4 /*yield*/, ctx.db.query("voiceStates").collect()];
case 1:
states = _d.sent();
grouped = {};
_i = 0, states_1 = states;
_d.label = 2;
case 2:
if (!(_i < states_1.length)) return [3 /*break*/, 9];
s = states_1[_i];
return [4 /*yield*/, ctx.db.get(s.userId)];
case 3:
user = _d.sent();
avatarUrl = null;
if (!(user === null || user === void 0 ? void 0 : user.avatarStorageId)) return [3 /*break*/, 5];
return [4 /*yield*/, (0, storageUrl_1.getPublicStorageUrl)(ctx, user.avatarStorageId)];
case 4:
avatarUrl = _d.sent();
_d.label = 5;
case 5:
joinSoundUrl = null;
if (!(user === null || user === void 0 ? void 0 : user.joinSoundStorageId)) return [3 /*break*/, 7];
return [4 /*yield*/, (0, storageUrl_1.getPublicStorageUrl)(ctx, user.joinSoundStorageId)];
case 6:
joinSoundUrl = _d.sent();
_d.label = 7;
case 7:
((_a = grouped[_c = s.channelId]) !== null && _a !== void 0 ? _a : (grouped[_c] = [])).push({
userId: s.userId,
username: s.username,
isMuted: s.isMuted,
isDeafened: s.isDeafened,
isScreenSharing: s.isScreenSharing,
isServerMuted: s.isServerMuted,
avatarUrl: avatarUrl,
joinSoundUrl: joinSoundUrl,
watchingStream: (_b = s.watchingStream) !== null && _b !== void 0 ? _b : null,
});
_d.label = 8;
case 8:
_i++;
return [3 /*break*/, 2];
case 9: return [2 /*return*/, grouped];
}
});
}); },
});
exports.afkMove = (0, server_1.mutation)({
args: {
userId: values_1.v.id("userProfiles"),
afkChannelId: values_1.v.id("channels"),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var settings, currentState, allStates, _i, allStates_2, s;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, ctx.db.query("serverSettings").first()];
case 1:
settings = _a.sent();
if (!settings || settings.afkChannelId !== args.afkChannelId) {
throw new Error("Invalid AFK channel");
}
return [4 /*yield*/, ctx.db
.query("voiceStates")
.withIndex("by_user", function (q) { return q.eq("userId", args.userId); })
.first()];
case 2:
currentState = _a.sent();
// No-op if not in voice or already in AFK channel
if (!currentState || currentState.channelId === args.afkChannelId)
return [2 /*return*/, null];
// Move to AFK channel: delete old state, insert new one muted
return [4 /*yield*/, ctx.db.delete(currentState._id)];
case 3:
// Move to AFK channel: delete old state, insert new one muted
_a.sent();
return [4 /*yield*/, ctx.db.insert("voiceStates", {
channelId: args.afkChannelId,
userId: args.userId,
username: currentState.username,
isMuted: true,
isDeafened: currentState.isDeafened,
isScreenSharing: false,
isServerMuted: currentState.isServerMuted,
})];
case 4:
_a.sent();
return [4 /*yield*/, ctx.db.query("voiceStates").collect()];
case 5:
allStates = _a.sent();
_i = 0, allStates_2 = allStates;
_a.label = 6;
case 6:
if (!(_i < allStates_2.length)) return [3 /*break*/, 9];
s = allStates_2[_i];
if (!(s.watchingStream === args.userId)) return [3 /*break*/, 8];
return [4 /*yield*/, ctx.db.patch(s._id, { watchingStream: undefined })];
case 7:
_a.sent();
_a.label = 8;
case 8:
_i++;
return [3 /*break*/, 6];
case 9: return [2 /*return*/, null];
}
});
}); },
});
exports.disconnectUser = (0, server_1.mutation)({
args: {
actorUserId: values_1.v.id("userProfiles"),
targetUserId: values_1.v.id("userProfiles"),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var roles, canMove, allStates, _i, allStates_3, s;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, (0, roles_1.getRolesForUser)(ctx, args.actorUserId)];
case 1:
roles = _a.sent();
canMove = roles.some(function (role) { var _a; return (_a = role.permissions) === null || _a === void 0 ? void 0 : _a["move_members"]; });
if (!canMove) {
throw new Error("You don't have permission to disconnect members");
}
return [4 /*yield*/, ctx.db.query("voiceStates").collect()];
case 2:
allStates = _a.sent();
_i = 0, allStates_3 = allStates;
_a.label = 3;
case 3:
if (!(_i < allStates_3.length)) return [3 /*break*/, 6];
s = allStates_3[_i];
if (!(s.watchingStream === args.targetUserId)) return [3 /*break*/, 5];
return [4 /*yield*/, ctx.db.patch(s._id, { watchingStream: undefined })];
case 4:
_a.sent();
_a.label = 5;
case 5:
_i++;
return [3 /*break*/, 3];
case 6: return [4 /*yield*/, removeUserVoiceStates(ctx, args.targetUserId)];
case 7:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});
exports.moveUser = (0, server_1.mutation)({
args: {
actorUserId: values_1.v.id("userProfiles"),
targetUserId: values_1.v.id("userProfiles"),
targetChannelId: values_1.v.id("channels"),
},
returns: values_1.v.null(),
handler: function (ctx, args) { return __awaiter(void 0, void 0, void 0, function () {
var roles, canMove, targetChannel, currentState;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, (0, roles_1.getRolesForUser)(ctx, args.actorUserId)];
case 1:
roles = _a.sent();
canMove = roles.some(function (role) { var _a; return (_a = role.permissions) === null || _a === void 0 ? void 0 : _a["move_members"]; });
if (!canMove) {
throw new Error("You don't have permission to move members");
}
return [4 /*yield*/, ctx.db.get(args.targetChannelId)];
case 2:
targetChannel = _a.sent();
if (!targetChannel)
throw new Error("Target channel not found");
if (targetChannel.type !== "voice")
throw new Error("Target channel is not a voice channel");
return [4 /*yield*/, ctx.db
.query("voiceStates")
.withIndex("by_user", function (q) { return q.eq("userId", args.targetUserId); })
.first()];
case 3:
currentState = _a.sent();
if (!currentState)
throw new Error("Target user is not in a voice channel");
// No-op if already in the target channel
if (currentState.channelId === args.targetChannelId)
return [2 /*return*/, null];
// Delete old voice state and insert new one preserving mute/deaf/screenshare
return [4 /*yield*/, ctx.db.delete(currentState._id)];
case 4:
// Delete old voice state and insert new one preserving mute/deaf/screenshare
_a.sent();
return [4 /*yield*/, ctx.db.insert("voiceStates", {
channelId: args.targetChannelId,
userId: args.targetUserId,
username: currentState.username,
isMuted: currentState.isMuted,
isDeafened: currentState.isDeafened,
isScreenSharing: currentState.isScreenSharing,
isServerMuted: currentState.isServerMuted,
})];
case 5:
_a.sent();
return [2 /*return*/, null];
}
});
}); },
});

View File

@@ -229,6 +229,35 @@ export const afkMove = mutation({
}, },
}); });
export const disconnectUser = mutation({
args: {
actorUserId: v.id("userProfiles"),
targetUserId: v.id("userProfiles"),
},
returns: v.null(),
handler: async (ctx, args) => {
const roles = await getRolesForUser(ctx, args.actorUserId);
const canMove = roles.some(
(role) => (role.permissions as Record<string, boolean>)?.["move_members"]
);
if (!canMove) {
throw new Error("You don't have permission to disconnect members");
}
// Clear viewers watching the target user's stream
const allStates = await ctx.db.query("voiceStates").collect();
for (const s of allStates) {
if (s.watchingStream === args.targetUserId) {
await ctx.db.patch(s._id, { watchingStream: undefined });
}
}
await removeUserVoiceStates(ctx, args.targetUserId);
return null;
},
});
export const moveUser = mutation({ export const moveUser = mutation({
args: { args: {
actorUserId: v.id("userProfiles"), actorUserId: v.id("userProfiles"),

View File

@@ -1,585 +0,0 @@
2026-02-13T18:56:03.9277925Z e0b0e1a10f48(version:v0.2.13) received task 41 of job build-and-release, be triggered by event: push
2026-02-13T18:56:03.9279801Z workflow prepared
2026-02-13T18:56:03.9280260Z evaluating expression 'success()'
2026-02-13T18:56:03.9280635Z expression 'success()' evaluated to 'true'
2026-02-13T18:56:03.9280737Z 🚀 Start image=moyettes/eb
2026-02-13T18:56:03.9315843Z 🐳 docker pull image=moyettes/eb platform= username= forcePull=false
2026-02-13T18:56:03.9315992Z 🐳 docker pull moyettes/eb
2026-02-13T18:56:03.9322701Z Image exists? true
2026-02-13T18:56:03.9421327Z Cleaning up network for job build-and-release, and network name is: GITEA-ACTIONS-TASK-41_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network
2026-02-13T18:56:04.5791683Z 🐳 docker create image=moyettes/eb platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="GITEA-ACTIONS-TASK-41_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network"
2026-02-13T18:56:04.5902212Z Custom container.Config from options ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image: Volumes:map[] WorkingDir: Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-02-13T18:56:04.5902464Z Merged container.Config ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=X64 RUNNER_TEMP=/tmp LANG=C.UTF-8] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image:moyettes/eb Volumes:map[] WorkingDir:/workspace/Moyettes/DiscordClone Entrypoint:[/bin/sleep 10800] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-02-13T18:56:04.5902671Z Custom container.HostConfig from options ==> &{Binds:[] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:GITEA-ACTIONS-TASK-41_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:false VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0xc000314948 OomKillDisable:0xc000314843 PidsLimit:0xc0003149a8 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-02-13T18:56:04.5902878Z --network and --net in the options will be ignored.
2026-02-13T18:56:04.5903023Z Merged container.HostConfig ==> &{Binds:[/var/run/docker.sock:/var/run/docker.sock] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:GITEA-ACTIONS-TASK-41_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:true VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0xc000314948 OomKillDisable:0xc000314843 PidsLimit:0xc0003149a8 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[{Type:volume Source:act-toolcache Target:/opt/hostedtoolcache ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-41_WORKFLOW-Build-and-Release_JOB-build-and-release-env Target:/var/run/act ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-41_WORKFLOW-Build-and-Release_JOB-build-and-release Target:/workspace/Moyettes/DiscordClone ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>}] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-02-13T18:56:06.1535282Z Created container name=GITEA-ACTIONS-TASK-41_WORKFLOW-Build-and-Release_JOB-build-and-release id=c552a4a1f514ed2e1c9ef47d4faf8897b005de0741436f0a80ede1e382a52974 from image moyettes/eb (platform: )
2026-02-13T18:56:06.1535514Z ENV ==> [RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=X64 RUNNER_TEMP=/tmp LANG=C.UTF-8]
2026-02-13T18:56:06.1535590Z 🐳 docker run image=moyettes/eb platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="GITEA-ACTIONS-TASK-41_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network"
2026-02-13T18:56:06.1535682Z Starting container: c552a4a1f514ed2e1c9ef47d4faf8897b005de0741436f0a80ede1e382a52974
2026-02-13T18:56:08.8613961Z Started container: c552a4a1f514ed2e1c9ef47d4faf8897b005de0741436f0a80ede1e382a52974
2026-02-13T18:56:08.9370013Z Writing entry to tarball workflow/event.json len:5058
2026-02-13T18:56:08.9370330Z Writing entry to tarball workflow/envs.txt len:0
2026-02-13T18:56:08.9370414Z Extracting content to '/var/run/act/'
2026-02-13T18:56:08.9433584Z ☁ git clone 'https://github.com/actions/checkout' # ref=v4
2026-02-13T18:56:08.9433717Z cloning https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T18:56:09.3391634Z Unable to pull refs/heads/v4: non-fast-forward update
2026-02-13T18:56:09.3391874Z Cloned https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T18:56:09.3503369Z Checked out v4
2026-02-13T18:56:09.3834837Z ☁ git clone 'https://github.com/actions/cache' # ref=v4
2026-02-13T18:56:09.3834986Z cloning https://github.com/actions/cache to /root/.cache/act/6b4e4eb40e21c1bd02cb00a273f4d79af7c42205c1390e4e65c594ecd7a3696e
2026-02-13T18:56:09.5963515Z Unable to pull refs/heads/v4: worktree contains unstaged changes
2026-02-13T18:56:09.5963817Z Cloned https://github.com/actions/cache to /root/.cache/act/6b4e4eb40e21c1bd02cb00a273f4d79af7c42205c1390e4e65c594ecd7a3696e
2026-02-13T18:56:09.6440660Z Checked out v4
2026-02-13T18:56:09.6545051Z evaluating expression ''
2026-02-13T18:56:09.6545296Z expression '' evaluated to 'true'
2026-02-13T18:56:09.6545356Z ⭐ Run Main Checkout repository
2026-02-13T18:56:09.6545443Z Writing entry to tarball workflow/outputcmd.txt len:0
2026-02-13T18:56:09.6545535Z Writing entry to tarball workflow/statecmd.txt len:0
2026-02-13T18:56:09.6545585Z Writing entry to tarball workflow/pathcmd.txt len:0
2026-02-13T18:56:09.6545636Z Writing entry to tarball workflow/envs.txt len:0
2026-02-13T18:56:09.6545677Z Writing entry to tarball workflow/SUMMARY.md len:0
2026-02-13T18:56:09.6545730Z Extracting content to '/var/run/act'
2026-02-13T18:56:09.6578043Z expression '${{ github.repository }}' rewritten to 'format('{0}', github.repository)'
2026-02-13T18:56:09.6578182Z evaluating expression 'format('{0}', github.repository)'
2026-02-13T18:56:09.6578404Z expression 'format('{0}', github.repository)' evaluated to '%!t(string=Moyettes/DiscordClone)'
2026-02-13T18:56:09.6578666Z expression '${{ github.token }}' rewritten to 'format('{0}', github.token)'
2026-02-13T18:56:09.6578737Z evaluating expression 'format('{0}', github.token)'
2026-02-13T18:56:09.6578885Z expression 'format('{0}', github.token)' evaluated to '%!t(string=***)'
2026-02-13T18:56:09.6579059Z type=remote-action actionDir=/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab actionPath= workdir=/workspace/Moyettes/DiscordClone actionCacheDir=/root/.cache/act actionName=c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab containerActionDir=/var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T18:56:09.6579185Z /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T18:56:09.6579323Z Removing /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/.gitignore before docker cp
2026-02-13T18:56:09.6579849Z 🐳 docker cp src=/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/ dst=/var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/
2026-02-13T18:56:09.6580477Z Writing tarball /tmp/act3990701342 from /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/
2026-02-13T18:56:09.6580587Z Stripping prefix:/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/ src:/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/
2026-02-13T18:56:09.6904677Z Extracting content from '/tmp/act3990701342' to '/var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/'
2026-02-13T18:56:09.7566990Z executing remote job container: [node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]
2026-02-13T18:56:09.7567337Z 🐳 docker exec cmd=[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js] user= workdir=
2026-02-13T18:56:09.7567416Z Exec command '[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]'
2026-02-13T18:56:09.7567627Z Working directory '/workspace/Moyettes/DiscordClone'
2026-02-13T18:56:09.8954730Z ::add-matcher::/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/problem-matcher.json
2026-02-13T18:56:09.8954830Z ::add-matcher::/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/problem-matcher.json
2026-02-13T18:56:09.8956435Z Syncing repository: Moyettes/DiscordClone
2026-02-13T18:56:09.8957540Z ::group::Getting Git version info
2026-02-13T18:56:09.8957739Z Working directory is '/workspace/Moyettes/DiscordClone'
2026-02-13T18:56:09.8970747Z [command]/usr/bin/git version
2026-02-13T18:56:09.9008141Z git version 2.34.1
2026-02-13T18:56:09.9024718Z ::endgroup::
2026-02-13T18:56:09.9031378Z Copying '/root/.gitconfig' to '/tmp/40732c9a-9cc3-4b96-b881-da6d2848b7b2/.gitconfig'
2026-02-13T18:56:09.9037647Z Temporarily overriding HOME='/tmp/40732c9a-9cc3-4b96-b881-da6d2848b7b2' before making global git config changes
2026-02-13T18:56:09.9037895Z Adding repository directory to the temporary git global config as a safe directory
2026-02-13T18:56:09.9041756Z [command]/usr/bin/git config --global --add safe.directory /workspace/Moyettes/DiscordClone
2026-02-13T18:56:09.9062633Z Deleting the contents of '/workspace/Moyettes/DiscordClone'
2026-02-13T18:56:09.9064767Z ::group::Initializing the repository
2026-02-13T18:56:09.9073312Z [command]/usr/bin/git init /workspace/Moyettes/DiscordClone
2026-02-13T18:56:09.9093068Z hint: Using 'master' as the name for the initial branch. This default branch name
2026-02-13T18:56:09.9093226Z hint: is subject to change. To configure the initial branch name to use in all
2026-02-13T18:56:09.9093297Z hint: of your new repositories, which will suppress this warning, call:
2026-02-13T18:56:09.9093373Z hint:
2026-02-13T18:56:09.9093441Z hint: git config --global init.defaultBranch <name>
2026-02-13T18:56:09.9093489Z hint:
2026-02-13T18:56:09.9093532Z hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
2026-02-13T18:56:09.9093578Z hint: 'development'. The just-created branch can be renamed via this command:
2026-02-13T18:56:09.9093625Z hint:
2026-02-13T18:56:09.9093662Z hint: git branch -m <name>
2026-02-13T18:56:09.9094450Z Initialized empty Git repository in /workspace/Moyettes/DiscordClone/.git/
2026-02-13T18:56:09.9099533Z [command]/usr/bin/git remote add origin http://192.168.125.15:4000/Moyettes/DiscordClone
2026-02-13T18:56:09.9115095Z ::endgroup::
2026-02-13T18:56:09.9115189Z ::group::Disabling automatic garbage collection
2026-02-13T18:56:09.9117322Z [command]/usr/bin/git config --local gc.auto 0
2026-02-13T18:56:09.9131021Z ::endgroup::
2026-02-13T18:56:09.9131118Z ::group::Setting up auth
2026-02-13T18:56:09.9134113Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
2026-02-13T18:56:09.9149874Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
2026-02-13T18:56:09.9313358Z [command]/usr/bin/git config --local --name-only --get-regexp http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader
2026-02-13T18:56:09.9328018Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader' && git config --local --unset-all 'http.http://192.168.125.15:4000/.extraheader' || :"
2026-02-13T18:56:09.9474257Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
2026-02-13T18:56:09.9488458Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
2026-02-13T18:56:09.9630037Z [command]/usr/bin/git config --local http.http://192.168.125.15:4000/.extraheader AUTHORIZATION: basic ***
2026-02-13T18:56:09.9645770Z ::endgroup::
2026-02-13T18:56:09.9645859Z ::group::Fetching the repository
2026-02-13T18:56:09.9649291Z [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +df9d0f2e4b62eb720eeb9808388ec06923a0296b:refs/remotes/origin/main
2026-02-13T18:56:12.4387290Z From http://192.168.125.15:4000/Moyettes/DiscordClone
2026-02-13T18:56:12.4387533Z * [new ref] df9d0f2e4b62eb720eeb9808388ec06923a0296b -> origin/main
2026-02-13T18:56:12.4400808Z ::endgroup::
2026-02-13T18:56:12.4400969Z ::group::Determining the checkout info
2026-02-13T18:56:12.4402089Z ::endgroup::
2026-02-13T18:56:12.4404843Z [command]/usr/bin/git sparse-checkout disable
2026-02-13T18:56:12.4426000Z [command]/usr/bin/git config --local --unset-all extensions.worktreeConfig
2026-02-13T18:56:12.4441690Z ::group::Checking out the ref
2026-02-13T18:56:12.4443971Z [command]/usr/bin/git checkout --progress --force -B main refs/remotes/origin/main
2026-02-13T18:56:12.5345548Z Switched to a new branch 'main'
2026-02-13T18:56:12.5345902Z Branch 'main' set up to track remote branch 'main' from 'origin'.
2026-02-13T18:56:12.5349777Z ::endgroup::
2026-02-13T18:56:12.5376410Z [command]/usr/bin/git log -1 --format=%H
2026-02-13T18:56:12.5394523Z df9d0f2e4b62eb720eeb9808388ec06923a0296b
2026-02-13T18:56:12.5402828Z ::remove-matcher owner=checkout-git::
2026-02-13T18:56:14.1972264Z (node:216) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
2026-02-13T18:56:14.1972406Z (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-13T18:56:34.2182156Z ::warning::Failed to restore: getCacheEntry failed: Request timeout: /_apis/artifactcache/cache?keys=npm-electron-3bfeb88fe20d0eab8d1e9deb0c16af4cc320445b15ebe7f6fa7d2e30a7316852%252Cnpm-electron-&version=f2531268ab9c19c75ce7b3eb23cc11c7f69fd3cf796834d4881591e430a373ff
2026-02-13T18:56:34.2183457Z Cache not found for input keys: npm-electron-3bfeb88fe20d0eab8d1e9deb0c16af4cc320445b15ebe7f6fa7d2e30a7316852, npm-electron-
2026-02-13T18:56:34.9567318Z npm warn deprecated tar@6.2.1: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T18:56:35.2078374Z npm warn deprecated npmlog@6.0.2: This package is no longer supported.
2026-02-13T18:56:46.9449453Z npm warn deprecated lodash.isequal@4.5.0: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
2026-02-13T18:56:47.1382638Z npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
2026-02-13T18:56:47.1991712Z npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T18:56:47.2034495Z npm warn deprecated gauge@4.0.4: This package is no longer supported.
2026-02-13T18:56:47.5067906Z npm warn deprecated boolean@3.2.0: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
2026-02-13T18:56:47.5186423Z npm warn deprecated are-we-there-yet@3.0.1: This package is no longer supported.
2026-02-13T18:56:47.8245140Z npm warn deprecated @npmcli/move-file@2.0.1: This functionality has been moved to @npmcli/fs
2026-02-13T18:56:48.4403127Z npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
2026-02-13T18:56:48.4782352Z npm warn deprecated glob@9.3.5: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T18:56:48.5665447Z npm warn deprecated glob@10.5.0: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T18:56:48.5681963Z npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
2026-02-13T18:56:48.5751339Z npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T18:56:48.5937092Z npm warn deprecated glob@8.1.0: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T18:56:48.5980233Z npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
2026-02-13T18:56:51.1811306Z
2026-02-13T18:56:51.1811605Z added 677 packages, and audited 683 packages in 17s
2026-02-13T18:56:51.1811714Z
2026-02-13T18:56:51.1811770Z 183 packages are looking for funding
2026-02-13T18:56:51.1811897Z run `npm fund` for details
2026-02-13T18:56:51.2002954Z
2026-02-13T18:56:51.2003207Z 11 vulnerabilities (1 moderate, 10 high)
2026-02-13T18:56:51.2003281Z
2026-02-13T18:56:51.2003321Z To address all issues (including breaking changes), run:
2026-02-13T18:56:51.2003366Z npm audit fix --force
2026-02-13T18:56:51.2003405Z
2026-02-13T18:56:51.2003444Z Run `npm audit` for details.
2026-02-13T18:56:51.4285812Z
2026-02-13T18:56:51.4286152Z > @discord-clone/electron@1.0.14 build
2026-02-13T18:56:51.4286212Z > vite build
2026-02-13T18:56:51.4286284Z
2026-02-13T18:56:51.5751519Z vite v7.3.1 building client environment for production...
2026-02-13T18:56:51.6098207Z transforming...
2026-02-13T18:56:51.9336851Z The glob option "as" has been deprecated in favour of "query". Please update `as: 'url'` to `query: '?url', import: 'default'`.
2026-02-13T18:56:52.0020219Z [plugin vite:esbuild] ../../packages/shared/src/components/ScreenShareModal.jsx: Duplicate key "width" in object literal
2026-02-13T18:56:52.0020649Z 71 | }}
2026-02-13T18:56:52.0020738Z 72 | >
2026-02-13T18:56:52.0020832Z 73 | ...le={{ position: 'relative', width: '100%', height: '250px', width: '450px', borderRadius: '8px', overflow: 'hidden...
2026-02-13T18:56:52.0020931Z | ^
2026-02-13T18:56:52.0021002Z 74 | {/* Thumbnail/Placeholder */}
2026-02-13T18:56:52.0021076Z 75 | {item.thumbnail ? (
2026-02-13T18:56:52.0021147Z 
2026-02-13T18:56:53.8260295Z ✓ 4636 modules transformed.
2026-02-13T18:56:54.3598471Z rendering chunks...
2026-02-13T18:56:54.3996107Z computing gzip size...
2026-02-13T18:56:54.5742439Z dist-react/index.html  0.63 kB │ gzip: 0.39 kB
2026-02-13T18:56:54.5742839Z dist-react/assets/woman_teacher-CXwESYU3.svg  4.10 kB │ gzip: 1.60 kB
2026-02-13T18:56:54.5743043Z dist-react/assets/woman_teacher_tone2-DWJ6rjnf.svg  4.10 kB │ gzip: 1.60 kB
2026-02-13T18:56:54.5743102Z dist-react/assets/woman_teacher_tone3-BvnMOsM7.svg  4.10 kB │ gzip: 1.60 kB
2026-02-13T18:56:54.5743157Z dist-react/assets/woman_teacher_tone4-C9bkU449.svg  4.10 kB │ gzip: 1.60 kB
2026-02-13T18:56:54.5743205Z dist-react/assets/bubble_tea-Cy1d5egt.svg  4.10 kB │ gzip: 1.92 kB
2026-02-13T18:56:54.5743263Z dist-react/assets/flag_gq-B3TFx5qI.svg  4.11 kB │ gzip: 1.68 kB
2026-02-13T18:56:54.5743314Z dist-react/assets/person_in_lotus_position-9VFgclqE.svg  4.11 kB │ gzip: 1.73 kB
2026-02-13T18:56:54.5743380Z dist-react/assets/person_in_lotus_position_tone1-MK18jaNb.svg  4.11 kB │ gzip: 1.73 kB
2026-02-13T18:56:54.5743434Z dist-react/assets/person_in_lotus_position_tone2-Dzm_xqT2.svg  4.11 kB │ gzip: 1.73 kB
2026-02-13T18:56:54.5743484Z dist-react/assets/person_in_lotus_position_tone4-BtjpxNGo.svg  4.11 kB │ gzip: 1.72 kB
2026-02-13T18:56:54.5743541Z dist-react/assets/person_in_lotus_position_tone3-DUxbd8tQ.svg  4.11 kB │ gzip: 1.73 kB
2026-02-13T18:56:54.5743594Z dist-react/assets/person_in_lotus_position_tone5-CinKf4VL.svg  4.11 kB │ gzip: 1.72 kB
2026-02-13T18:56:54.5743652Z dist-react/assets/man_feeding_baby_tone5-DOWKsCGX.svg  4.11 kB │ gzip: 1.83 kB
2026-02-13T18:56:54.5743708Z dist-react/assets/man_feeding_baby-BLUtslbF.svg  4.13 kB │ gzip: 1.83 kB
2026-02-13T18:56:54.5743765Z dist-react/assets/man_feeding_baby_tone1-lg4dBAV2.svg  4.13 kB │ gzip: 1.83 kB
2026-02-13T18:56:54.5743818Z dist-react/assets/man_feeding_baby_tone2-BuF25R9x.svg  4.13 kB │ gzip: 1.83 kB
2026-02-13T18:56:54.5743870Z dist-react/assets/man_feeding_baby_tone3-DEYxzqY-.svg  4.13 kB │ gzip: 1.83 kB
2026-02-13T18:56:54.5743938Z dist-react/assets/man_feeding_baby_tone4-IRS8MZPe.svg  4.13 kB │ gzip: 1.83 kB
2026-02-13T18:56:54.5743985Z dist-react/assets/woman_zombie-Cn4gQ0af.svg  4.15 kB │ gzip: 1.73 kB
2026-02-13T18:56:54.5744041Z dist-react/assets/hiking_boot-CPXD60gE.svg  4.17 kB │ gzip: 1.76 kB
2026-02-13T18:56:54.5744106Z dist-react/assets/japanese_ogre-BsqNvmIl.svg  4.22 kB │ gzip: 1.85 kB
2026-02-13T18:56:54.5744162Z dist-react/assets/woman_police_officer_tone5-CuQMgf5h.svg  4.23 kB │ gzip: 1.72 kB
2026-02-13T18:56:54.5744216Z dist-react/assets/woman_police_officer-D6jKOTyC.svg  4.25 kB │ gzip: 1.73 kB
2026-02-13T18:56:54.5744271Z dist-react/assets/woman_police_officer_tone1-A8sdhmvt.svg  4.25 kB │ gzip: 1.73 kB
2026-02-13T18:56:54.5744409Z dist-react/assets/woman_police_officer_tone2-DaHNn5-D.svg  4.25 kB │ gzip: 1.73 kB
2026-02-13T18:56:54.5744462Z dist-react/assets/woman_police_officer_tone3-DXZ2OdUI.svg  4.25 kB │ gzip: 1.73 kB
2026-02-13T18:56:54.5744520Z dist-react/assets/woman_police_officer_tone4--Oe8w2XD.svg  4.25 kB │ gzip: 1.72 kB
2026-02-13T18:56:54.5744568Z dist-react/assets/ferris_wheel-DvW0t9g3.svg  4.25 kB │ gzip: 1.19 kB
2026-02-13T18:56:54.5744622Z dist-react/assets/man_teacher_tone5-Bk9uZHaS.svg  4.26 kB │ gzip: 1.75 kB
2026-02-13T18:56:54.5744675Z dist-react/assets/man_teacher-BRypTuYs.svg  4.27 kB │ gzip: 1.75 kB
2026-02-13T18:56:54.5744723Z dist-react/assets/man_teacher_tone1-jNO2AiRD.svg  4.27 kB │ gzip: 1.75 kB
2026-02-13T18:56:54.5744787Z dist-react/assets/man_teacher_tone2-rQoIFfFz.svg  4.27 kB │ gzip: 1.75 kB
2026-02-13T18:56:54.5744841Z dist-react/assets/man_teacher_tone3-BEE8k6p5.svg  4.27 kB │ gzip: 1.75 kB
2026-02-13T18:56:54.5744889Z dist-react/assets/man_teacher_tone4-C4j4RIq3.svg  4.27 kB │ gzip: 1.74 kB
2026-02-13T18:56:54.5744942Z dist-react/assets/sari-BSRA0_R3.svg  4.27 kB │ gzip: 1.77 kB
2026-02-13T18:56:54.5745006Z dist-react/assets/gloves-BcY_RgAR.svg  4.31 kB │ gzip: 1.62 kB
2026-02-13T18:56:54.5745060Z dist-react/assets/moon_cake-BQr_VKRq.svg  4.31 kB │ gzip: 1.94 kB
2026-02-13T18:56:54.5745114Z dist-react/assets/man_zombie-D5T1AZ12.svg  4.34 kB │ gzip: 1.78 kB
2026-02-13T18:56:54.5745164Z dist-react/assets/woman_surfing_tone1-Mj4OdRWf.svg  4.35 kB │ gzip: 1.94 kB
2026-02-13T18:56:54.5745227Z dist-react/assets/woman_surfing-22i7hQgf.svg  4.38 kB │ gzip: 1.95 kB
2026-02-13T18:56:54.5745284Z dist-react/assets/woman_surfing_tone3-D1NUtDA8.svg  4.38 kB │ gzip: 1.95 kB
2026-02-13T18:56:54.5745340Z dist-react/assets/woman_surfing_tone2-BcJfdMyX.svg  4.38 kB │ gzip: 1.95 kB
2026-02-13T18:56:54.5745395Z dist-react/assets/woman_surfing_tone4-ByYdFJZp.svg  4.38 kB │ gzip: 1.95 kB
2026-02-13T18:56:54.5745462Z dist-react/assets/woman_surfing_tone5-CrN9a9WS.svg  4.38 kB │ gzip: 1.95 kB
2026-02-13T18:56:54.5745516Z dist-react/assets/person_with_probing_cane-DRcmbgmz.svg  4.38 kB │ gzip: 1.91 kB
2026-02-13T18:56:54.5745579Z dist-react/assets/person_with_probing_cane_tone1-zKjrapc7.svg  4.38 kB │ gzip: 1.90 kB
2026-02-13T18:56:54.5745639Z dist-react/assets/person_with_probing_cane_tone2-CY2wYkQb.svg  4.38 kB │ gzip: 1.91 kB
2026-02-13T18:56:54.5745702Z dist-react/assets/person_with_probing_cane_tone3-CdJAKQXv.svg  4.38 kB │ gzip: 1.91 kB
2026-02-13T18:56:54.5745755Z dist-react/assets/person_with_probing_cane_tone4-DXYc5Dlc.svg  4.38 kB │ gzip: 1.91 kB
2026-02-13T18:56:54.5745814Z dist-react/assets/person_with_probing_cane_tone5-DFAMgo57.svg  4.38 kB │ gzip: 1.91 kB
2026-02-13T18:56:54.5745868Z dist-react/assets/skier-BTSq18N5.svg  4.39 kB │ gzip: 1.75 kB
2026-02-13T18:56:54.5746002Z dist-react/assets/woman_singer_tone5-Co-5wXNK.svg  4.40 kB │ gzip: 1.84 kB
2026-02-13T18:56:54.5746057Z dist-react/assets/woman_singer_tone1-DbW2lM_k.svg  4.42 kB │ gzip: 1.85 kB
2026-02-13T18:56:54.5746114Z dist-react/assets/man_with_probing_cane-BarJlRlV.svg  4.45 kB │ gzip: 1.94 kB
2026-02-13T18:56:54.5746170Z dist-react/assets/man_with_probing_cane_tone2-CIeZuoUa.svg  4.45 kB │ gzip: 1.94 kB
2026-02-13T18:56:54.5746224Z dist-react/assets/man_with_probing_cane_tone3-GUZ14VpU.svg  4.45 kB │ gzip: 1.94 kB
2026-02-13T18:56:54.5746278Z dist-react/assets/man_with_probing_cane_tone1-D50RTI5B.svg  4.45 kB │ gzip: 1.94 kB
2026-02-13T18:56:54.5746331Z dist-react/assets/man_with_probing_cane_tone4-Bd6iA8-d.svg  4.45 kB │ gzip: 1.94 kB
2026-02-13T18:56:54.5746383Z dist-react/assets/man_with_probing_cane_tone5-BCpRNGU4.svg  4.45 kB │ gzip: 1.94 kB
2026-02-13T18:56:54.5746442Z dist-react/assets/woman_singer-skPaDBsj.svg  4.45 kB │ gzip: 1.84 kB
2026-02-13T18:56:54.5746500Z dist-react/assets/woman_singer_tone2-Bc-xqa4S.svg  4.45 kB │ gzip: 1.85 kB
2026-02-13T18:56:54.5746554Z dist-react/assets/woman_singer_tone3-CNRJeino.svg  4.45 kB │ gzip: 1.85 kB
2026-02-13T18:56:54.5746617Z dist-react/assets/woman_singer_tone4-CEfcWjkD.svg  4.45 kB │ gzip: 1.84 kB
2026-02-13T18:56:54.5746677Z dist-react/assets/flag_sz-CsAySmAn.svg  4.46 kB │ gzip: 1.66 kB
2026-02-13T18:56:54.5746730Z dist-react/assets/snowman2-CeWFCRvE.svg  4.48 kB │ gzip: 1.15 kB
2026-02-13T18:56:54.5746783Z dist-react/assets/man_surfing-fqnQ3hm1.svg  4.49 kB │ gzip: 1.97 kB
2026-02-13T18:56:54.5746836Z dist-react/assets/man_surfing_tone1-BeC3CjNB.svg  4.49 kB │ gzip: 1.96 kB
2026-02-13T18:56:54.5746893Z dist-react/assets/man_surfing_tone3-Dt-HUBR5.svg  4.49 kB │ gzip: 1.97 kB
2026-02-13T18:56:54.5746948Z dist-react/assets/man_surfing_tone4-PI8ASA2j.svg  4.49 kB │ gzip: 1.97 kB
2026-02-13T18:56:54.5747002Z dist-react/assets/man_surfing_tone5-CyvTB2HT.svg  4.49 kB │ gzip: 1.97 kB
2026-02-13T18:56:54.5747054Z dist-react/assets/man_surfing_tone2-CDUKGUjg.svg  4.49 kB │ gzip: 1.97 kB
2026-02-13T18:56:54.5747105Z dist-react/assets/motorized_wheelchair-DYMoavTH.svg  4.50 kB │ gzip: 1.63 kB
2026-02-13T18:56:54.5747162Z dist-react/assets/woman_feeding_baby_tone5-B6CmkSrw.svg  4.54 kB │ gzip: 2.01 kB
2026-02-13T18:56:54.5747220Z dist-react/assets/flag_ht-nORDdDQL.svg  4.54 kB │ gzip: 1.88 kB
2026-02-13T18:56:54.5747274Z dist-react/assets/woman_feeding_baby-p-8aPRtV.svg  4.56 kB │ gzip: 2.01 kB
2026-02-13T18:56:54.5747326Z dist-react/assets/woman_feeding_baby_tone1-BezQI8D_.svg  4.56 kB │ gzip: 2.01 kB
2026-02-13T18:56:54.5747384Z dist-react/assets/woman_feeding_baby_tone2-CwnQLRQK.svg  4.56 kB │ gzip: 2.01 kB
2026-02-13T18:56:54.5747453Z dist-react/assets/woman_feeding_baby_tone3-DlgTa1f-.svg  4.56 kB │ gzip: 2.01 kB
2026-02-13T18:56:54.5747517Z dist-react/assets/woman_feeding_baby_tone4-DVay5Top.svg  4.56 kB │ gzip: 2.01 kB
2026-02-13T18:56:54.5747763Z dist-react/assets/flag_ad-CYOJPtjR.svg  4.59 kB │ gzip: 1.79 kB
2026-02-13T18:56:54.5747825Z dist-react/assets/woman_with_probing_cane-DyJEphms.svg  4.60 kB │ gzip: 2.00 kB
2026-02-13T18:56:54.5747881Z dist-react/assets/woman_with_probing_cane_tone1-DuB7wHWP.svg  4.60 kB │ gzip: 2.00 kB
2026-02-13T18:56:54.5747939Z dist-react/assets/woman_with_probing_cane_tone2-GEDDmrTN.svg  4.60 kB │ gzip: 2.00 kB
2026-02-13T18:56:54.5747994Z dist-react/assets/woman_with_probing_cane_tone3-CeM4gv4f.svg  4.60 kB │ gzip: 2.00 kB
2026-02-13T18:56:54.5748049Z dist-react/assets/woman_with_probing_cane_tone4-BaEnBpNy.svg  4.60 kB │ gzip: 2.00 kB
2026-02-13T18:56:54.5748106Z dist-react/assets/woman_with_probing_cane_tone5-DimRSWot.svg  4.60 kB │ gzip: 2.00 kB
2026-02-13T18:56:54.5748161Z dist-react/assets/person_surfing_tone1-B_kCGm1b.svg  4.62 kB │ gzip: 2.10 kB
2026-02-13T18:56:54.5748222Z dist-react/assets/person_surfing-DEOz-TJs.svg  4.62 kB │ gzip: 2.10 kB
2026-02-13T18:56:54.5748275Z dist-react/assets/person_surfing_tone2-D35jmuhN.svg  4.62 kB │ gzip: 2.11 kB
2026-02-13T18:56:54.5748329Z dist-react/assets/person_surfing_tone3-C60a5Aj1.svg  4.62 kB │ gzip: 2.10 kB
2026-02-13T18:56:54.5748382Z dist-react/assets/person_surfing_tone4-nIpCJUpE.svg  4.62 kB │ gzip: 2.10 kB
2026-02-13T18:56:54.5748438Z dist-react/assets/person_surfing_tone5-CXXi5x8s.svg  4.62 kB │ gzip: 2.10 kB
2026-02-13T18:56:54.5748497Z dist-react/assets/flag_tc-Dn_lC0KY.svg  4.72 kB │ gzip: 1.94 kB
2026-02-13T18:56:54.5748549Z dist-react/assets/person_golfing-Mc5PuEC7.svg  4.72 kB │ gzip: 2.15 kB
2026-02-13T18:56:54.5748603Z dist-react/assets/person_golfing_tone2-BbPQ5nEE.svg  4.72 kB │ gzip: 2.15 kB
2026-02-13T18:56:54.5748654Z dist-react/assets/person_golfing_tone1-DhUQwNf7.svg  4.72 kB │ gzip: 2.15 kB
2026-02-13T18:56:54.5748710Z dist-react/assets/person_golfing_tone3-CKRqu9yJ.svg  4.72 kB │ gzip: 2.15 kB
2026-02-13T18:56:54.5748763Z dist-react/assets/person_golfing_tone4-DPEV2yNK.svg  4.72 kB │ gzip: 2.15 kB
2026-02-13T18:56:54.5748821Z dist-react/assets/person_golfing_tone5-Bgq3Ud_d.svg  4.72 kB │ gzip: 2.15 kB
2026-02-13T18:56:54.5748878Z dist-react/assets/flag_ki-Ccc3Xi24.svg  4.77 kB │ gzip: 1.66 kB
2026-02-13T18:56:54.5748931Z dist-react/assets/flag_mo-PAf1BQIO.svg  4.77 kB │ gzip: 2.24 kB
2026-02-13T18:56:54.5748990Z dist-react/assets/flag_sh-CT89bJZi.svg  4.80 kB │ gzip: 1.67 kB
2026-02-13T18:56:54.5749044Z dist-react/assets/crab-D6qU1zIW.svg  4.87 kB │ gzip: 1.97 kB
2026-02-13T18:56:54.5749097Z dist-react/assets/couple-KSrP6fk0.svg  4.90 kB │ gzip: 2.04 kB
2026-02-13T18:56:54.5749150Z dist-react/assets/kiwi-BWXr7Vjo.svg  4.91 kB │ gzip: 2.27 kB
2026-02-13T18:56:54.5749209Z dist-react/assets/hedgehog-CMNxZzfp.svg  4.92 kB │ gzip: 1.82 kB
2026-02-13T18:56:54.5749263Z dist-react/assets/money_with_wings-BnGoAiwj.svg  4.95 kB │ gzip: 2.07 kB
2026-02-13T18:56:54.5749320Z dist-react/assets/flag_kg-D_P2G_Do.svg  4.98 kB │ gzip: 2.14 kB
2026-02-13T18:56:54.5749430Z dist-react/assets/maracas-kQiWhg0J.svg  4.99 kB │ gzip: 1.76 kB
2026-02-13T18:56:54.5749487Z dist-react/assets/x_ray-CWrdKTDm.svg  4.99 kB │ gzip: 2.37 kB
2026-02-13T18:56:54.5749545Z dist-react/assets/people_holding_hands-BRZihiu5.svg  5.03 kB │ gzip: 1.68 kB
2026-02-13T18:56:54.5749602Z dist-react/assets/butterfly-AxzpD-Pg.svg  5.04 kB │ gzip: 1.86 kB
2026-02-13T18:56:54.5749658Z dist-react/assets/flag_xk-D1vfCqOd.svg  5.08 kB │ gzip: 2.15 kB
2026-02-13T18:56:54.5749711Z dist-react/assets/flag_tm-_4vioey7.svg  5.13 kB │ gzip: 1.54 kB
2026-02-13T18:56:54.5749765Z dist-react/assets/two_men_holding_hands-BKJxHZb_.svg  5.15 kB │ gzip: 1.50 kB
2026-02-13T18:56:54.5749824Z dist-react/assets/seal-Djs0F0U5.svg  5.16 kB │ gzip: 2.18 kB
2026-02-13T18:56:54.5750001Z dist-react/assets/speak_no_evil-EoRZCJhS.svg  5.20 kB │ gzip: 2.27 kB
2026-02-13T18:56:54.5750059Z dist-react/assets/man_dancing_tone1-XI7g5maV.svg  5.22 kB │ gzip: 2.00 kB
2026-02-13T18:56:54.5750113Z dist-react/assets/man_dancing_tone2-DBvANdsI.svg  5.22 kB │ gzip: 2.00 kB
2026-02-13T18:56:54.5750168Z dist-react/assets/man_dancing_tone3-BK7ka3J3.svg  5.22 kB │ gzip: 2.00 kB
2026-02-13T18:56:54.5750224Z dist-react/assets/man_dancing-Dg-6O6t7.svg  5.22 kB │ gzip: 2.00 kB
2026-02-13T18:56:54.5750280Z dist-react/assets/man_dancing_tone4-D9FZRxV5.svg  5.22 kB │ gzip: 2.00 kB
2026-02-13T18:56:54.5750331Z dist-react/assets/man_dancing_tone5-CQh9niVO.svg  5.22 kB │ gzip: 2.00 kB
2026-02-13T18:56:54.5750386Z dist-react/assets/flag_pf-OA_PTTaZ.svg  5.22 kB │ gzip: 1.81 kB
2026-02-13T18:56:54.5750442Z dist-react/assets/pie-DZ6nmSau.svg  5.24 kB │ gzip: 1.98 kB
2026-02-13T18:56:54.5750496Z dist-react/assets/two_women_holding_hands-CnKtKnaZ.svg  5.29 kB │ gzip: 1.71 kB
2026-02-13T18:56:54.5750552Z dist-react/assets/brain-Czvux5Q4.svg  5.32 kB │ gzip: 2.48 kB
2026-02-13T18:56:54.5750604Z dist-react/assets/lacrosse-DK95k1kF.svg  5.33 kB │ gzip: 2.22 kB
2026-02-13T18:56:54.5750656Z dist-react/assets/see_no_evil-DnuksPIR.svg  5.42 kB │ gzip: 2.38 kB
2026-02-13T18:56:54.5750712Z dist-react/assets/flag_vi-vzZjsoBi.svg  5.43 kB │ gzip: 2.34 kB
2026-02-13T18:56:54.5750764Z dist-react/assets/dodo-CoZFlciJ.svg  5.44 kB │ gzip: 2.31 kB
2026-02-13T18:56:54.5750818Z dist-react/assets/flag_hk-CzNuCBPg.svg  5.46 kB │ gzip: 2.52 kB
2026-02-13T18:56:54.5750873Z dist-react/assets/spider_web-DPcv-q20.svg  5.50 kB │ gzip: 2.43 kB
2026-02-13T18:56:54.5750929Z dist-react/assets/flag_bl-BoaeaHPp.svg  5.58 kB │ gzip: 1.90 kB
2026-02-13T18:56:54.5750981Z dist-react/assets/flag_as-B43i20pO.svg  5.61 kB │ gzip: 2.39 kB
2026-02-13T18:56:54.5751026Z dist-react/assets/flag_gp-DW1UVBGw.svg  5.63 kB │ gzip: 2.05 kB
2026-02-13T18:56:54.5751084Z dist-react/assets/flag_ic-BrB5Xakj.svg  5.63 kB │ gzip: 2.24 kB
2026-02-13T18:56:54.5751136Z dist-react/assets/man_golfing-DhSLN6KQ.svg  5.64 kB │ gzip: 2.61 kB
2026-02-13T18:56:54.5751198Z dist-react/assets/man_golfing_tone3-vxj0o6sI.svg  5.64 kB │ gzip: 2.61 kB
2026-02-13T18:56:54.5751250Z dist-react/assets/man_golfing_tone1-DBE1f7b1.svg  5.64 kB │ gzip: 2.61 kB
2026-02-13T18:56:54.5751305Z dist-react/assets/man_golfing_tone2-CNmTGsfk.svg  5.64 kB │ gzip: 2.61 kB
2026-02-13T18:56:54.5751404Z dist-react/assets/man_golfing_tone4-CsGYmisz.svg  5.64 kB │ gzip: 2.61 kB
2026-02-13T18:56:54.5751470Z dist-react/assets/man_golfing_tone5-Cf_z4uyg.svg  5.64 kB │ gzip: 2.60 kB
2026-02-13T18:56:54.5751526Z dist-react/assets/flag_bm-CzSakp_Z.svg  5.67 kB │ gzip: 2.37 kB
2026-02-13T18:56:54.5751569Z dist-react/assets/map-BGXvLkiw.svg  5.68 kB │ gzip: 2.62 kB
2026-02-13T18:56:54.5751626Z dist-react/assets/people_wrestling-DjCLlDDS.svg  5.71 kB │ gzip: 2.48 kB
2026-02-13T18:56:54.5751672Z dist-react/assets/tamale-2biJGrAo.svg  5.74 kB │ gzip: 2.53 kB
2026-02-13T18:56:54.5751727Z dist-react/assets/men_wrestling-BNuLmHCV.svg  5.74 kB │ gzip: 2.47 kB
2026-02-13T18:56:54.5751779Z dist-react/assets/empty_nest-DGy7reBo.svg  5.79 kB │ gzip: 2.71 kB
2026-02-13T18:56:54.5751831Z dist-react/assets/worm-CxRJMG1n.svg  5.86 kB │ gzip: 2.77 kB
2026-02-13T18:56:54.5751883Z dist-react/assets/flag_bo-B7hNQ755.svg  5.91 kB │ gzip: 2.22 kB
2026-02-13T18:56:54.5751940Z dist-react/assets/face_in_clouds-DBzCKo8S.svg  5.94 kB │ gzip: 2.63 kB
2026-02-13T18:56:54.5751995Z dist-react/assets/women_wrestling-CARP3ZvF.svg  5.96 kB │ gzip: 2.59 kB
2026-02-13T18:56:54.5752046Z dist-react/assets/man_lifting_weights_tone5-cnCH-jDP.svg  6.00 kB │ gzip: 2.06 kB
2026-02-13T18:56:54.5752106Z dist-react/assets/man_lifting_weights-DkiBT0IO.svg  6.03 kB │ gzip: 2.07 kB
2026-02-13T18:56:54.5752165Z dist-react/assets/man_lifting_weights_tone1-DGilOf2d.svg  6.03 kB │ gzip: 2.06 kB
2026-02-13T18:56:54.5752280Z dist-react/assets/man_lifting_weights_tone2-CXUv2fBp.svg  6.03 kB │ gzip: 2.07 kB
2026-02-13T18:56:54.5752334Z dist-react/assets/man_lifting_weights_tone4-MLQqpJKZ.svg  6.03 kB │ gzip: 2.07 kB
2026-02-13T18:56:54.5752391Z dist-react/assets/man_lifting_weights_tone3-DhF3q93u.svg  6.03 kB │ gzip: 2.07 kB
2026-02-13T18:56:54.5752446Z dist-react/assets/flag_fj-B2-D6gPQ.svg  6.04 kB │ gzip: 2.64 kB
2026-02-13T18:56:54.5752646Z dist-react/assets/flag_pn-Bde7vecB.svg  6.05 kB │ gzip: 2.68 kB
2026-02-13T18:56:54.5752704Z dist-react/assets/flag_bt-COHVTZ6I.svg  6.06 kB │ gzip: 2.52 kB
2026-02-13T18:56:54.5752755Z dist-react/assets/person_doing_cartwheel-B6e7BEW_.svg  6.07 kB │ gzip: 2.65 kB
2026-02-13T18:56:54.5752811Z dist-react/assets/person_doing_cartwheel_tone2-BR4ztGzg.svg  6.07 kB │ gzip: 2.65 kB
2026-02-13T18:56:54.5752869Z dist-react/assets/person_doing_cartwheel_tone1-TBt_b-Oj.svg  6.07 kB │ gzip: 2.65 kB
2026-02-13T18:56:54.5752924Z dist-react/assets/person_doing_cartwheel_tone4-j074vq-9.svg  6.07 kB │ gzip: 2.65 kB
2026-02-13T18:56:54.5752981Z dist-react/assets/person_doing_cartwheel_tone3-BzmNF0vv.svg  6.07 kB │ gzip: 2.65 kB
2026-02-13T18:56:54.5753034Z dist-react/assets/person_doing_cartwheel_tone5-BzNEt2oA.svg  6.07 kB │ gzip: 2.65 kB
2026-02-13T18:56:54.5753095Z dist-react/assets/accordion-BPueGNgN.svg  6.07 kB │ gzip: 1.20 kB
2026-02-13T18:56:54.5753150Z dist-react/assets/lobster-Cfls8jg_.svg  6.07 kB │ gzip: 2.32 kB
2026-02-13T18:56:54.5753209Z dist-react/assets/volcano-Bh_Lqk9r.svg  6.14 kB │ gzip: 2.70 kB
2026-02-13T18:56:54.5753270Z dist-react/assets/man_cartwheeling-NFQt9ZB9.svg  6.17 kB │ gzip: 2.69 kB
2026-02-13T18:56:54.5753324Z dist-react/assets/man_cartwheeling_tone1-B3S_eUE1.svg  6.17 kB │ gzip: 2.69 kB
2026-02-13T18:56:54.5753378Z dist-react/assets/man_cartwheeling_tone3-D2kqEChS.svg  6.17 kB │ gzip: 2.69 kB
2026-02-13T18:56:54.5753437Z dist-react/assets/man_cartwheeling_tone2-CYBBI2iM.svg  6.17 kB │ gzip: 2.69 kB
2026-02-13T18:56:54.5753493Z dist-react/assets/man_cartwheeling_tone4-B96D58fZ.svg  6.17 kB │ gzip: 2.69 kB
2026-02-13T18:56:54.5753547Z dist-react/assets/man_cartwheeling_tone5-PFLWmq7Q.svg  6.17 kB │ gzip: 2.69 kB
2026-02-13T18:56:54.5753600Z dist-react/assets/flag_lb-DHr4ylgr.svg  6.19 kB │ gzip: 2.75 kB
2026-02-13T18:56:54.5753656Z dist-react/assets/man_playing_handball_tone1-22QBgB92.svg  6.21 kB │ gzip: 2.62 kB
2026-02-13T18:56:54.5753709Z dist-react/assets/man_playing_handball_tone2-Bs8PtV12.svg  6.21 kB │ gzip: 2.62 kB
2026-02-13T18:56:54.5753765Z dist-react/assets/man_playing_handball-C_yN7fGQ.svg  6.21 kB │ gzip: 2.62 kB
2026-02-13T18:56:54.5753821Z dist-react/assets/man_playing_handball_tone3-q-BDso_I.svg  6.21 kB │ gzip: 2.62 kB
2026-02-13T18:56:54.5753873Z dist-react/assets/man_playing_handball_tone4-BUH96fLA.svg  6.21 kB │ gzip: 2.62 kB
2026-02-13T18:56:54.5753933Z dist-react/assets/man_playing_handball_tone5-DK-UJ5SH.svg  6.21 kB │ gzip: 2.62 kB
2026-02-13T18:56:54.5753987Z dist-react/assets/flag_fk-1KKBtSFw.svg  6.22 kB │ gzip: 2.54 kB
2026-02-13T18:56:54.5754043Z dist-react/assets/flag_rs-CmpxaRIS.svg  6.23 kB │ gzip: 2.35 kB
2026-02-13T18:56:54.5754095Z dist-react/assets/cucumber-oVkPYVB9.svg  6.24 kB │ gzip: 2.01 kB
2026-02-13T18:56:54.5754149Z dist-react/assets/woman_lifting_weights_tone5-BJQrRdVE.svg  6.25 kB │ gzip: 2.15 kB
2026-02-13T18:56:54.5754202Z dist-react/assets/woman_lifting_weights_tone1-BpRsBk7z.svg  6.28 kB │ gzip: 2.15 kB
2026-02-13T18:56:54.5754259Z dist-react/assets/woman_lifting_weights_tone2-P18Nfbuz.svg  6.28 kB │ gzip: 2.15 kB
2026-02-13T18:56:54.5754312Z dist-react/assets/woman_lifting_weights-CsixMYFL.svg  6.28 kB │ gzip: 2.15 kB
2026-02-13T18:56:54.5754380Z dist-react/assets/woman_lifting_weights_tone4-CQZmiYUl.svg  6.28 kB │ gzip: 2.16 kB
2026-02-13T18:56:54.5754433Z dist-react/assets/woman_lifting_weights_tone3-C0gnGp49.svg  6.28 kB │ gzip: 2.15 kB
2026-02-13T18:56:54.5754487Z dist-react/assets/person_lifting_weights_tone5-DEciUSJH.svg  6.29 kB │ gzip: 2.20 kB
2026-02-13T18:56:54.5754567Z dist-react/assets/person_lifting_weights_tone1-CXfKAA0L.svg  6.30 kB │ gzip: 2.19 kB
2026-02-13T18:56:54.5754638Z dist-react/assets/person_lifting_weights-Cn0dQ6qY.svg  6.30 kB │ gzip: 2.20 kB
2026-02-13T18:56:54.5754704Z dist-react/assets/person_lifting_weights_tone2-Dkw3-09P.svg  6.30 kB │ gzip: 2.20 kB
2026-02-13T18:56:54.5754780Z dist-react/assets/person_lifting_weights_tone4-C62SuN24.svg  6.30 kB │ gzip: 2.20 kB
2026-02-13T18:56:54.5754838Z dist-react/assets/person_lifting_weights_tone3-3OqiHF7e.svg  6.30 kB │ gzip: 2.20 kB
2026-02-13T18:56:54.5754893Z dist-react/assets/women_with_bunny_ears_partying-CKr9TLic.svg  6.30 kB │ gzip: 1.68 kB
2026-02-13T18:56:54.5754946Z dist-react/assets/flag_sm-BYO1ASeM.svg  6.31 kB │ gzip: 2.30 kB
2026-02-13T18:56:54.5754999Z dist-react/assets/coat-Cbu3wnI6.svg  6.35 kB │ gzip: 2.52 kB
2026-02-13T18:56:54.5755052Z dist-react/assets/woman_cartwheeling-tGvm940R.svg  6.37 kB │ gzip: 2.78 kB
2026-02-13T18:56:54.5755103Z dist-react/assets/woman_cartwheeling_tone1-fJFXi2hD.svg  6.37 kB │ gzip: 2.78 kB
2026-02-13T18:56:54.5755167Z dist-react/assets/woman_cartwheeling_tone2-C5lE2K9g.svg  6.37 kB │ gzip: 2.77 kB
2026-02-13T18:56:54.5755221Z dist-react/assets/woman_cartwheeling_tone4-CjyM2w54.svg  6.37 kB │ gzip: 2.77 kB
2026-02-13T18:56:54.5755275Z dist-react/assets/woman_cartwheeling_tone3-BourpL3A.svg  6.37 kB │ gzip: 2.77 kB
2026-02-13T18:56:54.5755335Z dist-react/assets/woman_cartwheeling_tone5-D-eW47Ua.svg  6.37 kB │ gzip: 2.77 kB
2026-02-13T18:56:54.5755399Z dist-react/assets/man_running-Bp7fZpx0.svg  6.37 kB │ gzip: 2.89 kB
2026-02-13T18:56:54.5755452Z dist-react/assets/man_running_tone1-BbRoQah0.svg  6.37 kB │ gzip: 2.89 kB
2026-02-13T18:56:54.5755519Z dist-react/assets/man_running_tone2-gBe1A9EP.svg  6.37 kB │ gzip: 2.89 kB
2026-02-13T18:56:54.5755594Z dist-react/assets/man_running_tone3-DfAx9qZO.svg  6.37 kB │ gzip: 2.88 kB
2026-02-13T18:56:54.5755820Z dist-react/assets/man_running_tone4-CeeXJkX_.svg  6.37 kB │ gzip: 2.88 kB
2026-02-13T18:56:54.5755889Z dist-react/assets/man_running_tone5-Do-aIXEX.svg  6.37 kB │ gzip: 2.88 kB
2026-02-13T18:56:54.5755958Z dist-react/assets/woman_playing_handball_tone1-B_P42W0r.svg  6.42 kB │ gzip: 2.73 kB
2026-02-13T18:56:54.5756023Z dist-react/assets/woman_playing_handball-fiyPmBDz.svg  6.42 kB │ gzip: 2.73 kB
2026-02-13T18:56:54.5756077Z dist-react/assets/woman_playing_handball_tone2-BtTxnxhZ.svg  6.42 kB │ gzip: 2.73 kB
2026-02-13T18:56:54.5756133Z dist-react/assets/woman_playing_handball_tone3-C7TXAAWV.svg  6.42 kB │ gzip: 2.72 kB
2026-02-13T18:56:54.5756186Z dist-react/assets/woman_playing_handball_tone4-CtCwRGCv.svg  6.42 kB │ gzip: 2.73 kB
2026-02-13T18:56:54.5756240Z dist-react/assets/woman_playing_handball_tone5-CmZlugee.svg  6.42 kB │ gzip: 2.73 kB
2026-02-13T18:56:54.5756296Z dist-react/assets/microbe-DHWlm4x3.svg  6.48 kB │ gzip: 2.82 kB
2026-02-13T18:56:54.5756348Z dist-react/assets/horse_racing-Cd5KXigQ.svg  6.50 kB │ gzip: 2.87 kB
2026-02-13T18:56:54.5757442Z dist-react/assets/horse_racing_tone1-BPFu29EM.svg  6.50 kB │ gzip: 2.87 kB
2026-02-13T18:56:54.5760546Z dist-react/assets/horse_racing_tone2-kHM6lt0G.svg  6.50 kB │ gzip: 2.88 kB
2026-02-13T18:56:54.5760767Z dist-react/assets/horse_racing_tone3-1prjoMK9.svg  6.50 kB │ gzip: 2.88 kB
2026-02-13T18:56:54.5761315Z dist-react/assets/horse_racing_tone4-DZVx5-VD.svg  6.50 kB │ gzip: 2.87 kB
2026-02-13T18:56:54.5761996Z dist-react/assets/horse_racing_tone5-DoKtvypB.svg  6.50 kB │ gzip: 2.88 kB
2026-02-13T18:56:54.5762193Z dist-react/assets/person_in_manual_wheelchair-B2ofcHYu.svg  6.50 kB │ gzip: 2.57 kB
2026-02-13T18:56:54.5762355Z dist-react/assets/person_in_manual_wheelchair_tone1-BrR0l2XR.svg  6.50 kB │ gzip: 2.57 kB
2026-02-13T18:56:54.5763623Z dist-react/assets/person_in_manual_wheelchair_tone2-DmJ1Zffk.svg  6.50 kB │ gzip: 2.57 kB
2026-02-13T18:56:54.5763954Z dist-react/assets/person_in_manual_wheelchair_tone3-Bt_5AaRy.svg  6.50 kB │ gzip: 2.57 kB
2026-02-13T18:56:54.5764075Z dist-react/assets/person_in_manual_wheelchair_tone4-TZTDWyKD.svg  6.50 kB │ gzip: 2.57 kB
2026-02-13T18:56:54.5764178Z dist-react/assets/person_in_manual_wheelchair_tone5-DrOKlCDl.svg  6.50 kB │ gzip: 2.57 kB
2026-02-13T18:56:54.5764280Z dist-react/assets/burrito-B4L0kbwK.svg  6.52 kB │ gzip: 2.58 kB
2026-02-13T18:56:54.5764373Z dist-react/assets/person_running-DNDUEkxU.svg  6.52 kB │ gzip: 2.94 kB
2026-02-13T18:56:54.5764461Z dist-react/assets/person_running_tone1-B8sLRwke.svg  6.52 kB │ gzip: 2.94 kB
2026-02-13T18:56:54.5764558Z dist-react/assets/person_running_tone2-DNzEDUb0.svg  6.52 kB │ gzip: 2.94 kB
2026-02-13T18:56:54.5764644Z dist-react/assets/person_running_tone3-Dist2leS.svg  6.52 kB │ gzip: 2.94 kB
2026-02-13T18:56:54.5764737Z dist-react/assets/person_running_tone4-DVBWC3-p.svg  6.52 kB │ gzip: 2.94 kB
2026-02-13T18:56:54.5764831Z dist-react/assets/person_running_tone5-DEOJVy8u.svg  6.52 kB │ gzip: 2.93 kB
2026-02-13T18:56:54.5764922Z dist-react/assets/man_in_manual_wheelchair_tone1-Da2hybrT.svg  6.57 kB │ gzip: 2.62 kB
2026-02-13T18:56:54.5765018Z dist-react/assets/man_in_manual_wheelchair-cGfKOLRc.svg  6.60 kB │ gzip: 2.63 kB
2026-02-13T18:56:54.5765114Z dist-react/assets/man_in_manual_wheelchair_tone2-BPBmkRcs.svg  6.60 kB │ gzip: 2.63 kB
2026-02-13T18:56:54.5765206Z dist-react/assets/man_in_manual_wheelchair_tone3-H5kpv3q_.svg  6.60 kB │ gzip: 2.63 kB
2026-02-13T18:56:54.5765291Z dist-react/assets/man_in_manual_wheelchair_tone4-BvKWPBcq.svg  6.60 kB │ gzip: 2.63 kB
2026-02-13T18:56:54.5765377Z dist-react/assets/man_in_manual_wheelchair_tone5-YZQTD5Nr.svg  6.60 kB │ gzip: 2.63 kB
2026-02-13T18:56:54.5765461Z dist-react/assets/person_playing_handball_tone1-CbOONp_g.svg  6.62 kB │ gzip: 2.66 kB
2026-02-13T18:56:54.5765554Z dist-react/assets/person_playing_handball-CH3hWpQR.svg  6.62 kB │ gzip: 2.66 kB
2026-02-13T18:56:54.5765641Z dist-react/assets/person_playing_handball_tone4-BsA09Avm.svg  6.62 kB │ gzip: 2.66 kB
2026-02-13T18:56:54.5765729Z dist-react/assets/person_playing_handball_tone2-jeC51_-P.svg  6.62 kB │ gzip: 2.66 kB
2026-02-13T18:56:54.5765819Z dist-react/assets/person_playing_handball_tone3-BGgWTsuS.svg  6.62 kB │ gzip: 2.66 kB
2026-02-13T18:56:54.5765904Z dist-react/assets/person_playing_handball_tone5-D_rmeJiN.svg  6.62 kB │ gzip: 2.66 kB
2026-02-13T18:56:54.5766001Z dist-react/assets/men_with_bunny_ears_partying-DabknRQ1.svg  6.64 kB │ gzip: 1.78 kB
2026-02-13T18:56:54.5766089Z dist-react/assets/man_bouncing_ball_tone1-BrCW39oq.svg  6.67 kB │ gzip: 3.05 kB
2026-02-13T18:56:54.5766175Z dist-react/assets/man_bouncing_ball-BCtAjpGP.svg  6.67 kB │ gzip: 3.04 kB
2026-02-13T18:56:54.5766259Z dist-react/assets/man_bouncing_ball_tone3-CMYhYDFZ.svg  6.67 kB │ gzip: 3.05 kB
2026-02-13T18:56:54.5766343Z dist-react/assets/man_bouncing_ball_tone2-pU3f7Oqo.svg  6.67 kB │ gzip: 3.05 kB
2026-02-13T18:56:54.5766429Z dist-react/assets/man_bouncing_ball_tone4-BonEB_V5.svg  6.67 kB │ gzip: 3.05 kB
2026-02-13T18:56:54.5766516Z dist-react/assets/man_bouncing_ball_tone5-mVU7qtFm.svg  6.67 kB │ gzip: 3.05 kB
2026-02-13T18:56:54.5766602Z dist-react/assets/woman_running-_mwbLWM0.svg  6.74 kB │ gzip: 3.03 kB
2026-02-13T18:56:54.5766690Z dist-react/assets/woman_running_tone1-Dfqdg043.svg  6.74 kB │ gzip: 3.03 kB
2026-02-13T18:56:54.5766775Z dist-react/assets/woman_running_tone2-rXRqTMa0.svg  6.74 kB │ gzip: 3.03 kB
2026-02-13T18:56:54.5766860Z dist-react/assets/woman_running_tone3-BmRDPwCM.svg  6.74 kB │ gzip: 3.03 kB
2026-02-13T18:56:54.5766942Z dist-react/assets/woman_running_tone4-DmFzAsxD.svg  6.74 kB │ gzip: 3.03 kB
2026-02-13T18:56:54.5767033Z dist-react/assets/woman_running_tone5-C66GYSAh.svg  6.74 kB │ gzip: 3.03 kB
2026-02-13T18:56:54.5767122Z dist-react/assets/woman_in_manual_wheelchair-Ba72kfnU.svg  6.75 kB │ gzip: 2.66 kB
2026-02-13T18:56:54.5767204Z dist-react/assets/woman_in_manual_wheelchair_tone1-Ce9x88Rf.svg  6.75 kB │ gzip: 2.66 kB
2026-02-13T18:56:54.5767291Z dist-react/assets/woman_in_manual_wheelchair_tone2-CAKIPnJE.svg  6.75 kB │ gzip: 2.66 kB
2026-02-13T18:56:54.5767378Z dist-react/assets/woman_in_manual_wheelchair_tone3-D4YsEoBp.svg  6.75 kB │ gzip: 2.66 kB
2026-02-13T18:56:54.5767464Z dist-react/assets/woman_in_manual_wheelchair_tone4-BD3k04p2.svg  6.75 kB │ gzip: 2.66 kB
2026-02-13T18:56:54.5767550Z dist-react/assets/woman_in_manual_wheelchair_tone5-BmBeJ4-f.svg  6.75 kB │ gzip: 2.66 kB
2026-02-13T18:56:54.5767642Z dist-react/assets/person_in_motorized_wheelchair-DxhhvjYe.svg  6.80 kB │ gzip: 2.69 kB
2026-02-13T18:56:54.5767809Z dist-react/assets/person_in_motorized_wheelchair_tone1-Dcta4qUb.svg  6.80 kB │ gzip: 2.68 kB
2026-02-13T18:56:54.5767902Z dist-react/assets/person_in_motorized_wheelchair_tone2-C8UQYonN.svg  6.80 kB │ gzip: 2.69 kB
2026-02-13T18:56:54.5767986Z dist-react/assets/person_in_motorized_wheelchair_tone3-BRD_Obbg.svg  6.80 kB │ gzip: 2.69 kB
2026-02-13T18:56:54.5768077Z dist-react/assets/person_in_motorized_wheelchair_tone4-DLSO0rlF.svg  6.80 kB │ gzip: 2.69 kB
2026-02-13T18:56:54.5768163Z dist-react/assets/person_in_motorized_wheelchair_tone5-SnULyxgF.svg  6.80 kB │ gzip: 2.69 kB
2026-02-13T18:56:54.5768244Z dist-react/assets/person_bouncing_ball-H1IsbPT2.svg  6.82 kB │ gzip: 3.10 kB
2026-02-13T18:56:54.5768326Z dist-react/assets/person_bouncing_ball_tone2-9V5mlEG0.svg  6.82 kB │ gzip: 3.10 kB
2026-02-13T18:56:54.5768408Z dist-react/assets/person_bouncing_ball_tone1-BIhBY2_P.svg  6.82 kB │ gzip: 3.10 kB
2026-02-13T18:56:54.5768960Z dist-react/assets/person_bouncing_ball_tone3-DSpJYpZ1.svg  6.82 kB │ gzip: 3.10 kB
2026-02-13T18:56:54.5769102Z dist-react/assets/person_bouncing_ball_tone4-BycyNnMy.svg  6.82 kB │ gzip: 3.10 kB
2026-02-13T18:56:54.5769171Z dist-react/assets/person_bouncing_ball_tone5-C9pS5gcg.svg  6.82 kB │ gzip: 3.10 kB
2026-02-13T18:56:54.5769231Z dist-react/assets/man_in_motorized_wheelchair_tone1-B-J_H3TB.svg  6.85 kB │ gzip: 2.71 kB
2026-02-13T18:56:54.5769291Z dist-react/assets/flag_ms-BKjfidu-.svg  6.86 kB │ gzip: 3.00 kB
2026-02-13T18:56:54.5769373Z dist-react/assets/man_in_motorized_wheelchair-CiMQlH-Z.svg  6.87 kB │ gzip: 2.72 kB
2026-02-13T18:56:54.5769434Z dist-react/assets/man_in_motorized_wheelchair_tone2-DQy0C3Cx.svg  6.87 kB │ gzip: 2.72 kB
2026-02-13T18:56:54.5769492Z dist-react/assets/man_in_motorized_wheelchair_tone3-DuduwQoe.svg  6.87 kB │ gzip: 2.72 kB
2026-02-13T18:56:54.5769549Z dist-react/assets/man_in_motorized_wheelchair_tone4-CoEn9n-F.svg  6.87 kB │ gzip: 2.72 kB
2026-02-13T18:56:54.5769606Z dist-react/assets/man_in_motorized_wheelchair_tone5-CgvQDAuT.svg  6.87 kB │ gzip: 2.72 kB
2026-02-13T18:56:54.5769659Z dist-react/assets/flag_ky-E8sT-Yzf.svg  6.99 kB │ gzip: 2.92 kB
2026-02-13T18:56:54.5769714Z dist-react/assets/anatomical_heart-DbQDqK_8.svg  7.00 kB │ gzip: 3.14 kB
2026-02-13T18:56:54.5769770Z dist-react/assets/wales-ll0ySOk-.svg  7.01 kB │ gzip: 2.89 kB
2026-02-13T18:56:54.5769824Z dist-react/assets/woman_in_motorized_wheelchair-CIaEP3y5.svg  7.03 kB │ gzip: 2.79 kB
2026-02-13T18:56:54.5769884Z dist-react/assets/woman_in_motorized_wheelchair_tone1-1BibIgKr.svg  7.03 kB │ gzip: 2.78 kB
2026-02-13T18:56:54.5769938Z dist-react/assets/woman_in_motorized_wheelchair_tone2-uhLYilhF.svg  7.03 kB │ gzip: 2.79 kB
2026-02-13T18:56:54.5769994Z dist-react/assets/woman_in_motorized_wheelchair_tone3-B51r71l0.svg  7.03 kB │ gzip: 2.79 kB
2026-02-13T18:56:54.5770049Z dist-react/assets/woman_in_motorized_wheelchair_tone4-oIvpxZcp.svg  7.03 kB │ gzip: 2.79 kB
2026-02-13T18:56:54.5770102Z dist-react/assets/woman_in_motorized_wheelchair_tone5-_fFN26h0.svg  7.03 kB │ gzip: 2.79 kB
2026-02-13T18:56:54.5770166Z dist-react/assets/woman_bouncing_ball-B4V8jGG-.svg  7.09 kB │ gzip: 3.20 kB
2026-02-13T18:56:54.5770219Z dist-react/assets/woman_bouncing_ball_tone2-BPyPsinZ.svg  7.09 kB │ gzip: 3.21 kB
2026-02-13T18:56:54.5770273Z dist-react/assets/woman_bouncing_ball_tone1-I7gUQpbX.svg  7.09 kB │ gzip: 3.21 kB
2026-02-13T18:56:54.5770327Z dist-react/assets/woman_bouncing_ball_tone4-CtQI59zT.svg  7.09 kB │ gzip: 3.21 kB
2026-02-13T18:56:54.5770385Z dist-react/assets/woman_bouncing_ball_tone3-UqVs8gxM.svg  7.09 kB │ gzip: 3.21 kB
2026-02-13T18:56:54.5770438Z dist-react/assets/woman_bouncing_ball_tone5-BgHu12i2.svg  7.09 kB │ gzip: 3.21 kB
2026-02-13T18:56:54.5770489Z dist-react/assets/flag_va-BB2uDrB0.svg  7.21 kB │ gzip: 2.49 kB
2026-02-13T18:56:54.5770541Z dist-react/assets/mammoth-Diaisynz.svg  7.29 kB │ gzip: 3.06 kB
2026-02-13T18:56:54.5770595Z dist-react/assets/nest_with_eggs-C5ulh3Rz.svg  7.36 kB │ gzip: 3.39 kB
2026-02-13T18:56:54.5770656Z dist-react/assets/phoenix-QKXqSCuH.svg  7.57 kB │ gzip: 2.91 kB
2026-02-13T18:56:54.5770712Z dist-react/assets/flag_cy-JKjUtxO9.svg  7.60 kB │ gzip: 3.32 kB
2026-02-13T18:56:54.5770764Z dist-react/assets/people_with_bunny_ears_partying-BVR6SBwD.svg  7.63 kB │ gzip: 1.93 kB
2026-02-13T18:56:54.5770822Z dist-react/assets/flag_gu-CyZZwWUz.svg  7.68 kB │ gzip: 3.24 kB
2026-02-13T18:56:54.5770877Z dist-react/assets/mute-CFH4QcSE.mp3  7.74 kB
2026-02-13T18:56:54.5770934Z dist-react/assets/t_rex-BYG-fgI4.svg  8.00 kB │ gzip: 3.35 kB
2026-02-13T18:56:54.5770987Z dist-react/assets/flag_vg-DWuAWiyw.svg  8.21 kB │ gzip: 1.91 kB
2026-02-13T18:56:54.5771040Z dist-react/assets/flag_yt-BfOxXbO5.svg  8.42 kB │ gzip: 2.95 kB
2026-02-13T18:56:54.5771092Z dist-react/assets/piñata-CQK6iMPe.svg  8.47 kB │ gzip: 3.21 kB
2026-02-13T18:56:54.5771148Z dist-react/assets/mirror_ball-R_criUm_.svg  8.55 kB │ gzip: 3.31 kB
2026-02-13T18:56:54.5771202Z dist-react/assets/ping-LfakLpwb.mp3  8.58 kB
2026-02-13T18:56:54.5771255Z dist-react/assets/flag_gs-DhFNtBGF.svg  8.86 kB │ gzip: 3.67 kB
2026-02-13T18:56:54.5771310Z dist-react/assets/knot-CpRGiIMe.svg  8.92 kB │ gzip: 3.89 kB
2026-02-13T18:56:54.5771365Z dist-react/assets/flag_dg-DwJEN7pv.svg  9.06 kB │ gzip: 2.87 kB
2026-02-13T18:56:54.5771423Z dist-react/assets/flag_gt-CietPgvg.svg  9.11 kB │ gzip: 3.86 kB
2026-02-13T18:56:54.5771475Z dist-react/assets/flag_mx-g-aNhK9D.svg  9.66 kB │ gzip: 3.72 kB
2026-02-13T18:56:54.5771529Z dist-react/assets/flag_ta-Q6DTxsoW.svg  10.30 kB │ gzip: 4.05 kB
2026-02-13T18:56:54.5771581Z dist-react/assets/flag_je-CGBxZBdT.svg  10.35 kB │ gzip: 4.17 kB
2026-02-13T18:56:54.5771635Z dist-react/assets/unmute-BDrGNM7s.mp3  10.67 kB
2026-02-13T18:56:54.5771698Z dist-react/assets/flag_do-sBcfT32z.svg  11.37 kB │ gzip: 4.67 kB
2026-02-13T18:56:54.5771751Z dist-react/assets/deafen-BWE6ozKl.mp3  11.92 kB
2026-02-13T18:56:54.5771806Z dist-react/assets/flag_sa-B3EC8eCD.svg  12.29 kB │ gzip: 5.12 kB
2026-02-13T18:56:54.5771859Z dist-react/assets/flag_al-D439po3l.svg  12.43 kB │ gzip: 5.32 kB
2026-02-13T18:56:54.5771914Z dist-react/assets/undeafen-DHeUZCMd.mp3  13.59 kB
2026-02-13T18:56:54.5771968Z dist-react/assets/flag_bz-B34xZjVJ.svg  13.64 kB │ gzip: 5.44 kB
2026-02-13T18:56:54.5772022Z dist-react/assets/flag_pm-C-C2d-w4.svg  13.65 kB │ gzip: 3.80 kB
2026-02-13T18:56:54.5772076Z dist-react/assets/leave_call-DtItyZMg.mp3  14.43 kB
2026-02-13T18:56:54.5772128Z dist-react/assets/flag_nf-BjOIhoMF.svg  14.69 kB │ gzip: 5.98 kB
2026-02-13T18:56:54.5772177Z dist-react/assets/screenshare_start-B8JctOq8.mp3  16.13 kB
2026-02-13T18:56:54.5772231Z dist-react/assets/flag_ac-Dr8n8VBW.svg  16.66 kB │ gzip: 5.62 kB
2026-02-13T18:56:54.5772286Z dist-react/assets/screenshare_stop-DhppajDk.mp3  18.43 kB
2026-02-13T18:56:54.5772344Z dist-react/assets/potted_plant-BHg6K0D8.svg  21.00 kB │ gzip: 8.96 kB
2026-02-13T18:56:54.5772397Z dist-react/assets/flag_mp-Bs0Xr_ND.svg  24.06 kB │ gzip: 9.57 kB
2026-02-13T18:56:54.5772452Z dist-react/assets/flag_af-CN78RMpg.svg  24.13 kB │ gzip: 9.18 kB
2026-02-13T18:56:54.5772504Z dist-react/assets/flag_kz-D77IkgDL.svg  26.58 kB │ gzip: 9.02 kB
2026-02-13T18:56:54.5772555Z dist-react/assets/united_nations-BC9awctQ.svg  26.58 kB │ gzip: 10.32 kB
2026-02-13T18:56:54.5772610Z dist-react/assets/join_call-B65a9Ev2.mp3  30.54 kB
2026-02-13T18:56:54.5772666Z dist-react/assets/gg sans Regular-Bd8GJPVd.woff  39.09 kB
2026-02-13T18:56:54.5772718Z dist-react/assets/emojies_greyscale-CtRIvx0g.png  39.11 kB
2026-02-13T18:56:54.5772770Z dist-react/assets/gg sans Bold-BGlwbW8t.woff  40.13 kB
2026-02-13T18:56:54.5772823Z dist-react/assets/gg sans Medium-BMWm4JFW.woff  40.32 kB
2026-02-13T18:56:54.5772875Z dist-react/assets/gg sans Semibold-xAGa8zYH.woff  40.57 kB
2026-02-13T18:56:54.5772931Z dist-react/assets/emojies_colored-Cxo2u_zo.png  45.89 kB
2026-02-13T18:56:54.5772985Z dist-react/assets/screenshare_viewer_join-BOPrADSV.mp3  67.54 kB
2026-02-13T18:56:54.5773038Z dist-react/assets/screenshare_viewer_leave-BoDMhfvJ.mp3  67.54 kB
2026-02-13T18:56:54.5773093Z dist-react/assets/index-DN2VsIhS.css  72.50 kB │ gzip: 12.96 kB
2026-02-13T18:56:54.5773153Z dist-react/assets/index-9riexBjR.js 8,425.21 kB │ gzip: 1,537.92 kB
2026-02-13T18:56:54.5773212Z 
2026-02-13T18:56:54.5773250Z (!) Some chunks are larger than 1000 kB after minification. Consider:
2026-02-13T18:56:54.5773298Z - Using dynamic import() to code-split the application
2026-02-13T18:56:54.5773340Z - Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
2026-02-13T18:56:54.5773394Z - Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
2026-02-13T18:56:54.5773447Z ✓ built in 2.99s
2026-02-13T18:56:55.0055993Z • electron-builder version=25.1.8 os=6.12.54-Unraid
2026-02-13T18:56:55.0057823Z • artifacts will be published if draft release exists reason=CI detected
2026-02-13T18:56:55.0077289Z • loaded configuration file=package.json ("build" field)
2026-02-13T18:56:55.0853845Z • installing production dependencies platform=linux arch=x64 appDir=/workspace/Moyettes/DiscordClone/apps/electron
2026-02-13T18:56:55.6702292Z  spawn /workspace/Moyettes/DiscordClone/node_modules/app-builder-bin/linux/x64/app-builder ENOENT failedTask=build stackTrace=Error: spawn /workspace/Moyettes/DiscordClone/node_modules/app-builder-bin/linux/x64/app-builder ENOENT
2026-02-13T18:56:55.6702645Z at Process.ChildProcess._handle.onexit (node:internal/child_process:285:19)
2026-02-13T18:56:55.6702714Z at onErrorNT (node:internal/child_process:483:16)
2026-02-13T18:56:55.6702763Z at processTicksAndRejections (node:internal/process/task_queues:90:21)
2026-02-13T18:56:55.6856601Z ❌ Failure - Main Build Electron app
2026-02-13T18:56:55.6882903Z exitcode '1': failure
2026-02-13T18:56:55.7208548Z expression 'npm-electron-${{ hashFiles('package-lock.json') }}' rewritten to 'format('npm-electron-{0}', hashFiles('package-lock.json'))'
2026-02-13T18:56:55.7208702Z evaluating expression 'format('npm-electron-{0}', hashFiles('package-lock.json'))'
2026-02-13T18:56:55.7208840Z Writing entry to tarball workflow/hashfiles/index.js len:168437
2026-02-13T18:56:55.7211203Z Extracting content to '/var/run/act'
2026-02-13T18:56:55.7221923Z 🐳 docker exec cmd=[node /var/run/act/workflow/hashfiles/index.js] user= workdir=
2026-02-13T18:56:55.7222288Z Exec command '[node /var/run/act/workflow/hashfiles/index.js]'
2026-02-13T18:56:55.7222419Z Working directory '/workspace/Moyettes/DiscordClone'
2026-02-13T18:56:55.7767128Z expression 'format('npm-electron-{0}', hashFiles('package-lock.json'))' evaluated to '%!t(string=npm-electron-3bfeb88fe20d0eab8d1e9deb0c16af4cc320445b15ebe7f6fa7d2e30a7316852)'
2026-02-13T18:56:55.7803356Z evaluating expression 'success()'
2026-02-13T18:56:55.7803627Z expression 'success()' evaluated to 'false'
2026-02-13T18:56:55.7803713Z Skipping step 'Cache npm and Electron' due to 'success()'
2026-02-13T18:56:55.7895634Z evaluating expression 'always()'
2026-02-13T18:56:55.7895891Z expression 'always()' evaluated to 'true'
2026-02-13T18:56:55.7895990Z ⭐ Run Post Checkout repository
2026-02-13T18:56:55.7896129Z Writing entry to tarball workflow/outputcmd.txt len:0
2026-02-13T18:56:55.7896262Z Writing entry to tarball workflow/statecmd.txt len:0
2026-02-13T18:56:55.7896349Z Writing entry to tarball workflow/pathcmd.txt len:0
2026-02-13T18:56:55.7896434Z Writing entry to tarball workflow/envs.txt len:0
2026-02-13T18:56:55.7896509Z Writing entry to tarball workflow/SUMMARY.md len:0
2026-02-13T18:56:55.7896596Z Extracting content to '/var/run/act'
2026-02-13T18:56:55.7906440Z run post step for 'Checkout repository'
2026-02-13T18:56:55.7906756Z executing remote job container: [node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]
2026-02-13T18:56:55.7906848Z 🐳 docker exec cmd=[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js] user= workdir=
2026-02-13T18:56:55.7906914Z Exec command '[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]'
2026-02-13T18:56:55.7907081Z Working directory '/workspace/Moyettes/DiscordClone'
2026-02-13T18:56:55.8792208Z [command]/usr/bin/git version
2026-02-13T18:56:55.8990232Z git version 2.34.1
2026-02-13T18:56:55.9000518Z ***
2026-02-13T18:56:55.9004484Z Copying '/root/.gitconfig' to '/tmp/51dee9ee-51c8-4042-bbdd-01a2df1f223b/.gitconfig'
2026-02-13T18:56:55.9009834Z Temporarily overriding HOME='/tmp/51dee9ee-51c8-4042-bbdd-01a2df1f223b' before making global git config changes
2026-02-13T18:56:55.9010645Z Adding repository directory to the temporary git global config as a safe directory
2026-02-13T18:56:55.9012991Z [command]/usr/bin/git config --global --add safe.directory /workspace/Moyettes/DiscordClone
2026-02-13T18:56:55.9032089Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
2026-02-13T18:56:55.9050125Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
2026-02-13T18:56:55.9208756Z [command]/usr/bin/git config --local --name-only --get-regexp http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader
2026-02-13T18:56:55.9219838Z http.http://192.168.125.15:4000/.extraheader
2026-02-13T18:56:55.9223833Z [command]/usr/bin/git config --local --unset-all http.http://192.168.125.15:4000/.extraheader
2026-02-13T18:56:55.9239391Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader' && git config --local --unset-all 'http.http://192.168.125.15:4000/.extraheader' || :"
2026-02-13T18:56:55.9392917Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
2026-02-13T18:56:55.9407020Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
2026-02-13T18:56:55.9590876Z ✅ Success - Post Checkout repository
2026-02-13T18:56:55.9612358Z Cleaning up container for job build-and-release
2026-02-13T18:57:55.9616842Z failed to remove container: Delete "http://%2Fvar%2Frun%2Fdocker.sock/v1.44/containers/c552a4a1f514ed2e1c9ef47d4faf8897b005de0741436f0a80ede1e382a52974?force=1&v=1": context deadline exceeded
2026-02-13T18:57:55.9617116Z Removed container: c552a4a1f514ed2e1c9ef47d4faf8897b005de0741436f0a80ede1e382a52974
2026-02-13T18:57:55.9617186Z Error while stop job container: context deadline exceeded
2026-02-13T18:57:55.9617239Z 🏁 Job failed
2026-02-13T18:57:55.9656964Z Error occurred running finally: Error occurred running finally: context deadline exceeded (original error: <nil>) (original error: <nil>)

View File

@@ -1,165 +0,0 @@
2026-02-13T19:32:53.7334568Z e0b0e1a10f48(version:v0.2.13) received task 44 of job build-and-release, be triggered by event: push
2026-02-13T19:32:53.7337270Z workflow prepared
2026-02-13T19:32:53.7337920Z evaluating expression 'success()'
2026-02-13T19:32:53.7338391Z expression 'success()' evaluated to 'true'
2026-02-13T19:32:53.7338550Z 🚀 Start image=moyettes/eb
2026-02-13T19:32:53.7380263Z 🐳 docker pull image=moyettes/eb platform= username= forcePull=false
2026-02-13T19:32:53.7380377Z 🐳 docker pull moyettes/eb
2026-02-13T19:32:53.7385010Z Image exists? true
2026-02-13T19:32:53.7461341Z Cleaning up network for job build-and-release, and network name is: GITEA-ACTIONS-TASK-44_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network
2026-02-13T19:32:54.4179621Z 🐳 docker create image=moyettes/eb platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="GITEA-ACTIONS-TASK-44_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network"
2026-02-13T19:32:54.4261171Z Custom container.Config from options ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image: Volumes:map[] WorkingDir: Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-02-13T19:32:54.4261486Z Merged container.Config ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=X64 RUNNER_TEMP=/tmp LANG=C.UTF-8] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image:moyettes/eb Volumes:map[] WorkingDir:/workspace/Moyettes/DiscordClone Entrypoint:[/bin/sleep 10800] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-02-13T19:32:54.4261827Z Custom container.HostConfig from options ==> &{Binds:[] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:GITEA-ACTIONS-TASK-44_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:false VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0xc0001fa3c8 OomKillDisable:0xc0001fa2c3 PidsLimit:0xc0001fa428 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-02-13T19:32:54.4262179Z --network and --net in the options will be ignored.
2026-02-13T19:32:54.4262424Z Merged container.HostConfig ==> &{Binds:[/var/run/docker.sock:/var/run/docker.sock] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:GITEA-ACTIONS-TASK-44_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:true VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0xc0001fa3c8 OomKillDisable:0xc0001fa2c3 PidsLimit:0xc0001fa428 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[{Type:volume Source:act-toolcache Target:/opt/hostedtoolcache ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-44_WORKFLOW-Build-and-Release_JOB-build-and-release-env Target:/var/run/act ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-44_WORKFLOW-Build-and-Release_JOB-build-and-release Target:/workspace/Moyettes/DiscordClone ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>}] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-02-13T19:32:55.8176538Z Created container name=GITEA-ACTIONS-TASK-44_WORKFLOW-Build-and-Release_JOB-build-and-release id=2debfc455c871161eed822c759604d5bde221adcab93035906c77fa9a3785090 from image moyettes/eb (platform: )
2026-02-13T19:32:55.8176747Z ENV ==> [RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=X64 RUNNER_TEMP=/tmp LANG=C.UTF-8]
2026-02-13T19:32:55.8176835Z 🐳 docker run image=moyettes/eb platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="GITEA-ACTIONS-TASK-44_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network"
2026-02-13T19:32:55.8176928Z Starting container: 2debfc455c871161eed822c759604d5bde221adcab93035906c77fa9a3785090
2026-02-13T19:32:58.8503743Z Started container: 2debfc455c871161eed822c759604d5bde221adcab93035906c77fa9a3785090
2026-02-13T19:32:58.9192530Z Writing entry to tarball workflow/event.json len:4862
2026-02-13T19:32:58.9192933Z Writing entry to tarball workflow/envs.txt len:0
2026-02-13T19:32:58.9193078Z Extracting content to '/var/run/act/'
2026-02-13T19:32:58.9271597Z ☁ git clone 'https://github.com/actions/checkout' # ref=v4
2026-02-13T19:32:58.9271776Z cloning https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T19:32:59.2987119Z Unable to pull refs/heads/v4: non-fast-forward update
2026-02-13T19:32:59.2987344Z Cloned https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T19:32:59.3124333Z Checked out v4
2026-02-13T19:32:59.3173710Z ☁ git clone 'https://github.com/actions/cache' # ref=v4
2026-02-13T19:32:59.3173863Z cloning https://github.com/actions/cache to /root/.cache/act/6b4e4eb40e21c1bd02cb00a273f4d79af7c42205c1390e4e65c594ecd7a3696e
2026-02-13T19:32:59.5255777Z Unable to pull refs/heads/v4: worktree contains unstaged changes
2026-02-13T19:32:59.5255989Z Cloned https://github.com/actions/cache to /root/.cache/act/6b4e4eb40e21c1bd02cb00a273f4d79af7c42205c1390e4e65c594ecd7a3696e
2026-02-13T19:32:59.5732184Z Checked out v4
2026-02-13T19:32:59.5839013Z evaluating expression ''
2026-02-13T19:32:59.5839259Z expression '' evaluated to 'true'
2026-02-13T19:32:59.5839318Z ⭐ Run Main Checkout repository
2026-02-13T19:32:59.5839403Z Writing entry to tarball workflow/outputcmd.txt len:0
2026-02-13T19:32:59.5839488Z Writing entry to tarball workflow/statecmd.txt len:0
2026-02-13T19:32:59.5839536Z Writing entry to tarball workflow/pathcmd.txt len:0
2026-02-13T19:32:59.5839583Z Writing entry to tarball workflow/envs.txt len:0
2026-02-13T19:32:59.5839629Z Writing entry to tarball workflow/SUMMARY.md len:0
2026-02-13T19:32:59.5839679Z Extracting content to '/var/run/act'
2026-02-13T19:32:59.5878878Z expression '${{ github.repository }}' rewritten to 'format('{0}', github.repository)'
2026-02-13T19:32:59.5879022Z evaluating expression 'format('{0}', github.repository)'
2026-02-13T19:32:59.5879251Z expression 'format('{0}', github.repository)' evaluated to '%!t(string=Moyettes/DiscordClone)'
2026-02-13T19:32:59.5879412Z expression '${{ github.token }}' rewritten to 'format('{0}', github.token)'
2026-02-13T19:32:59.5879488Z evaluating expression 'format('{0}', github.token)'
2026-02-13T19:32:59.5879626Z expression 'format('{0}', github.token)' evaluated to '%!t(string=***)'
2026-02-13T19:32:59.5880002Z type=remote-action actionDir=/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab actionPath= workdir=/workspace/Moyettes/DiscordClone actionCacheDir=/root/.cache/act actionName=c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab containerActionDir=/var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T19:32:59.5880134Z /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T19:32:59.5880271Z Removing /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/.gitignore before docker cp
2026-02-13T19:32:59.5880764Z 🐳 docker cp src=/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/ dst=/var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/
2026-02-13T19:32:59.5881407Z Writing tarball /tmp/act1688867230 from /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/
2026-02-13T19:32:59.5881517Z Stripping prefix:/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/ src:/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/
2026-02-13T19:32:59.6215690Z Extracting content from '/tmp/act1688867230' to '/var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/'
2026-02-13T19:32:59.6652539Z executing remote job container: [node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]
2026-02-13T19:32:59.6652906Z 🐳 docker exec cmd=[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js] user= workdir=
2026-02-13T19:32:59.6652986Z Exec command '[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]'
2026-02-13T19:32:59.6653186Z Working directory '/workspace/Moyettes/DiscordClone'
2026-02-13T19:32:59.7589420Z ::add-matcher::/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/problem-matcher.json
2026-02-13T19:32:59.7589499Z ::add-matcher::/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/problem-matcher.json
2026-02-13T19:32:59.7591129Z Syncing repository: Moyettes/DiscordClone
2026-02-13T19:32:59.7592301Z ::group::Getting Git version info
2026-02-13T19:32:59.7592477Z Working directory is '/workspace/Moyettes/DiscordClone'
2026-02-13T19:32:59.7604667Z [command]/usr/bin/git version
2026-02-13T19:32:59.7632808Z git version 2.34.1
2026-02-13T19:32:59.7642833Z ::endgroup::
2026-02-13T19:32:59.7647656Z Copying '/root/.gitconfig' to '/tmp/138033a9-6df9-46f3-a309-e608610a1345/.gitconfig'
2026-02-13T19:32:59.7651244Z Temporarily overriding HOME='/tmp/138033a9-6df9-46f3-a309-e608610a1345' before making global git config changes
2026-02-13T19:32:59.7651330Z Adding repository directory to the temporary git global config as a safe directory
2026-02-13T19:32:59.7653938Z [command]/usr/bin/git config --global --add safe.directory /workspace/Moyettes/DiscordClone
2026-02-13T19:32:59.7673993Z Deleting the contents of '/workspace/Moyettes/DiscordClone'
2026-02-13T19:32:59.7680064Z ::group::Initializing the repository
2026-02-13T19:32:59.7681486Z [command]/usr/bin/git init /workspace/Moyettes/DiscordClone
2026-02-13T19:32:59.7702970Z hint: Using 'master' as the name for the initial branch. This default branch name
2026-02-13T19:32:59.7703162Z hint: is subject to change. To configure the initial branch name to use in all
2026-02-13T19:32:59.7703252Z hint: of your new repositories, which will suppress this warning, call:
2026-02-13T19:32:59.7703336Z hint:
2026-02-13T19:32:59.7703421Z hint: git config --global init.defaultBranch <name>
2026-02-13T19:32:59.7703499Z hint:
2026-02-13T19:32:59.7703562Z hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
2026-02-13T19:32:59.7703635Z hint: 'development'. The just-created branch can be renamed via this command:
2026-02-13T19:32:59.7703711Z hint:
2026-02-13T19:32:59.7703776Z hint: git branch -m <name>
2026-02-13T19:32:59.7706673Z Initialized empty Git repository in /workspace/Moyettes/DiscordClone/.git/
2026-02-13T19:32:59.7714124Z [command]/usr/bin/git remote add origin http://192.168.125.15:4000/Moyettes/DiscordClone
2026-02-13T19:32:59.7732694Z ::endgroup::
2026-02-13T19:32:59.7732878Z ::group::Disabling automatic garbage collection
2026-02-13T19:32:59.7734538Z [command]/usr/bin/git config --local gc.auto 0
2026-02-13T19:32:59.7748794Z ::endgroup::
2026-02-13T19:32:59.7748891Z ::group::Setting up auth
2026-02-13T19:32:59.7753015Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
2026-02-13T19:32:59.7768050Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
2026-02-13T19:32:59.7931408Z [command]/usr/bin/git config --local --name-only --get-regexp http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader
2026-02-13T19:32:59.7945319Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader' && git config --local --unset-all 'http.http://192.168.125.15:4000/.extraheader' || :"
2026-02-13T19:32:59.8093184Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
2026-02-13T19:32:59.8106922Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
2026-02-13T19:32:59.8252768Z [command]/usr/bin/git config --local http.http://192.168.125.15:4000/.extraheader AUTHORIZATION: basic ***
2026-02-13T19:32:59.8269559Z ::endgroup::
2026-02-13T19:32:59.8269664Z ::group::Fetching the repository
2026-02-13T19:32:59.8272971Z [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +e61df284e909fb14b5b88c1309704f76820910e5:refs/remotes/origin/main
2026-02-13T19:33:02.2026273Z From http://192.168.125.15:4000/Moyettes/DiscordClone
2026-02-13T19:33:02.2026536Z * [new ref] e61df284e909fb14b5b88c1309704f76820910e5 -> origin/main
2026-02-13T19:33:02.2039430Z ::endgroup::
2026-02-13T19:33:02.2039550Z ::group::Determining the checkout info
2026-02-13T19:33:02.2040586Z ::endgroup::
2026-02-13T19:33:02.2043493Z [command]/usr/bin/git sparse-checkout disable
2026-02-13T19:33:02.2065932Z [command]/usr/bin/git config --local --unset-all extensions.worktreeConfig
2026-02-13T19:33:02.2082220Z ::group::Checking out the ref
2026-02-13T19:33:02.2084395Z [command]/usr/bin/git checkout --progress --force -B main refs/remotes/origin/main
2026-02-13T19:33:02.2947591Z Switched to a new branch 'main'
2026-02-13T19:33:02.2948234Z Branch 'main' set up to track remote branch 'main' from 'origin'.
2026-02-13T19:33:02.2952747Z ::endgroup::
2026-02-13T19:33:02.2979370Z [command]/usr/bin/git log -1 --format=%H
2026-02-13T19:33:02.2999322Z e61df284e909fb14b5b88c1309704f76820910e5
2026-02-13T19:33:02.3007012Z ::remove-matcher owner=checkout-git::
2026-02-13T19:33:03.3278336Z (node:217) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
2026-02-13T19:33:03.3278474Z (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-13T19:33:23.3475905Z ::warning::Failed to restore: getCacheEntry failed: Request timeout: /_apis/artifactcache/cache?keys=npm-electron-3bfeb88fe20d0eab8d1e9deb0c16af4cc320445b15ebe7f6fa7d2e30a7316852%252Cnpm-electron-&version=f2531268ab9c19c75ce7b3eb23cc11c7f69fd3cf796834d4881591e430a373ff
2026-02-13T19:33:23.3477842Z Cache not found for input keys: npm-electron-3bfeb88fe20d0eab8d1e9deb0c16af4cc320445b15ebe7f6fa7d2e30a7316852, npm-electron-
2026-02-13T19:33:23.4124247Z /var/run/act/workflow/3.sh: 2: set: Illegal option -o pipefail
2026-02-13T19:33:23.4127538Z ❌ Failure - Main Install dependencies
2026-02-13T19:33:23.4147342Z exitcode '2': failure
2026-02-13T19:33:23.4638842Z expression 'npm-electron-${{ hashFiles('package-lock.json') }}' rewritten to 'format('npm-electron-{0}', hashFiles('package-lock.json'))'
2026-02-13T19:33:23.4639046Z evaluating expression 'format('npm-electron-{0}', hashFiles('package-lock.json'))'
2026-02-13T19:33:23.4639254Z Writing entry to tarball workflow/hashfiles/index.js len:168437
2026-02-13T19:33:23.4640133Z Extracting content to '/var/run/act'
2026-02-13T19:33:23.4649485Z 🐳 docker exec cmd=[node /var/run/act/workflow/hashfiles/index.js] user= workdir=
2026-02-13T19:33:23.4649638Z Exec command '[node /var/run/act/workflow/hashfiles/index.js]'
2026-02-13T19:33:23.4649734Z Working directory '/workspace/Moyettes/DiscordClone'
2026-02-13T19:33:23.5271132Z expression 'format('npm-electron-{0}', hashFiles('package-lock.json'))' evaluated to '%!t(string=npm-electron-3bfeb88fe20d0eab8d1e9deb0c16af4cc320445b15ebe7f6fa7d2e30a7316852)'
2026-02-13T19:33:23.5310392Z evaluating expression 'success()'
2026-02-13T19:33:23.5310589Z expression 'success()' evaluated to 'false'
2026-02-13T19:33:23.5310671Z Skipping step 'Cache npm and Electron' due to 'success()'
2026-02-13T19:33:23.5404097Z evaluating expression 'always()'
2026-02-13T19:33:23.5404401Z expression 'always()' evaluated to 'true'
2026-02-13T19:33:23.5404499Z ⭐ Run Post Checkout repository
2026-02-13T19:33:23.5404646Z Writing entry to tarball workflow/outputcmd.txt len:0
2026-02-13T19:33:23.5404779Z Writing entry to tarball workflow/statecmd.txt len:0
2026-02-13T19:33:23.5404873Z Writing entry to tarball workflow/pathcmd.txt len:0
2026-02-13T19:33:23.5404977Z Writing entry to tarball workflow/envs.txt len:0
2026-02-13T19:33:23.5405054Z Writing entry to tarball workflow/SUMMARY.md len:0
2026-02-13T19:33:23.5405173Z Extracting content to '/var/run/act'
2026-02-13T19:33:23.5414329Z run post step for 'Checkout repository'
2026-02-13T19:33:23.5414678Z executing remote job container: [node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]
2026-02-13T19:33:23.5414778Z 🐳 docker exec cmd=[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js] user= workdir=
2026-02-13T19:33:23.5414840Z Exec command '[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]'
2026-02-13T19:33:23.5415001Z Working directory '/workspace/Moyettes/DiscordClone'
2026-02-13T19:33:23.6310631Z [command]/usr/bin/git version
2026-02-13T19:33:23.6331724Z git version 2.34.1
2026-02-13T19:33:23.6350074Z ***
2026-02-13T19:33:23.6356242Z Copying '/root/.gitconfig' to '/tmp/8d698909-39ad-4750-b3be-314148a4aa23/.gitconfig'
2026-02-13T19:33:23.6363154Z Temporarily overriding HOME='/tmp/8d698909-39ad-4750-b3be-314148a4aa23' before making global git config changes
2026-02-13T19:33:23.6363740Z Adding repository directory to the temporary git global config as a safe directory
2026-02-13T19:33:23.6367434Z [command]/usr/bin/git config --global --add safe.directory /workspace/Moyettes/DiscordClone
2026-02-13T19:33:23.6390138Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
2026-02-13T19:33:23.6407326Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
2026-02-13T19:33:23.6572241Z [command]/usr/bin/git config --local --name-only --get-regexp http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader
2026-02-13T19:33:23.6584724Z http.http://192.168.125.15:4000/.extraheader
2026-02-13T19:33:23.6590389Z [command]/usr/bin/git config --local --unset-all http.http://192.168.125.15:4000/.extraheader
2026-02-13T19:33:23.6606735Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader' && git config --local --unset-all 'http.http://192.168.125.15:4000/.extraheader' || :"
2026-02-13T19:33:23.6754982Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
2026-02-13T19:33:23.6768951Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
2026-02-13T19:33:23.6959873Z ✅ Success - Post Checkout repository
2026-02-13T19:33:23.6976704Z Cleaning up container for job build-and-release

View File

@@ -1,198 +0,0 @@
2026-02-13T19:36:21.9777754Z e0b0e1a10f48(version:v0.2.13) received task 45 of job build-and-release, be triggered by event: push
2026-02-13T19:36:21.9779832Z workflow prepared
2026-02-13T19:36:21.9780279Z evaluating expression 'success()'
2026-02-13T19:36:21.9780699Z expression 'success()' evaluated to 'true'
2026-02-13T19:36:21.9780795Z 🚀 Start image=moyettes/eb
2026-02-13T19:36:21.9819365Z 🐳 docker pull image=moyettes/eb platform= username= forcePull=false
2026-02-13T19:36:21.9819476Z 🐳 docker pull moyettes/eb
2026-02-13T19:36:21.9823902Z Image exists? true
2026-02-13T19:36:21.9901448Z Cleaning up network for job build-and-release, and network name is: GITEA-ACTIONS-TASK-45_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network
2026-02-13T19:36:22.6931880Z 🐳 docker create image=moyettes/eb platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="GITEA-ACTIONS-TASK-45_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network"
2026-02-13T19:36:22.7012819Z Custom container.Config from options ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image: Volumes:map[] WorkingDir: Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-02-13T19:36:22.7013111Z Merged container.Config ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=X64 RUNNER_TEMP=/tmp LANG=C.UTF-8] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image:moyettes/eb Volumes:map[] WorkingDir:/workspace/Moyettes/DiscordClone Entrypoint:[/bin/sleep 10800] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-02-13T19:36:22.7013481Z Custom container.HostConfig from options ==> &{Binds:[] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:GITEA-ACTIONS-TASK-45_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:false VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0xc0001fa3c8 OomKillDisable:0xc0001fa2c3 PidsLimit:0xc0001fa428 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-02-13T19:36:22.7013825Z --network and --net in the options will be ignored.
2026-02-13T19:36:22.7014035Z Merged container.HostConfig ==> &{Binds:[/var/run/docker.sock:/var/run/docker.sock] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:GITEA-ACTIONS-TASK-45_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:true VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0xc0001fa3c8 OomKillDisable:0xc0001fa2c3 PidsLimit:0xc0001fa428 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[{Type:volume Source:act-toolcache Target:/opt/hostedtoolcache ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-45_WORKFLOW-Build-and-Release_JOB-build-and-release-env Target:/var/run/act ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-45_WORKFLOW-Build-and-Release_JOB-build-and-release Target:/workspace/Moyettes/DiscordClone ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>}] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-02-13T19:36:23.8673081Z Created container name=GITEA-ACTIONS-TASK-45_WORKFLOW-Build-and-Release_JOB-build-and-release id=d548f196e47e927667dc2c54fadbced669d7e0cedf25618a1d405c92768e9fb1 from image moyettes/eb (platform: )
2026-02-13T19:36:23.8673324Z ENV ==> [RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=X64 RUNNER_TEMP=/tmp LANG=C.UTF-8]
2026-02-13T19:36:23.8673409Z 🐳 docker run image=moyettes/eb platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="GITEA-ACTIONS-TASK-45_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network"
2026-02-13T19:36:23.8673519Z Starting container: d548f196e47e927667dc2c54fadbced669d7e0cedf25618a1d405c92768e9fb1
2026-02-13T19:36:26.7000331Z Started container: d548f196e47e927667dc2c54fadbced669d7e0cedf25618a1d405c92768e9fb1
2026-02-13T19:36:26.7853339Z Writing entry to tarball workflow/event.json len:4892
2026-02-13T19:36:26.7853724Z Writing entry to tarball workflow/envs.txt len:0
2026-02-13T19:36:26.7853844Z Extracting content to '/var/run/act/'
2026-02-13T19:36:26.7928425Z ☁ git clone 'https://github.com/actions/checkout' # ref=v4
2026-02-13T19:36:26.7928632Z cloning https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T19:36:27.3331108Z Unable to pull refs/heads/v4: non-fast-forward update
2026-02-13T19:36:27.3331493Z Cloned https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T19:36:27.3466578Z Checked out v4
2026-02-13T19:36:27.3511622Z ☁ git clone 'https://github.com/actions/cache' # ref=v4
2026-02-13T19:36:27.3511799Z cloning https://github.com/actions/cache to /root/.cache/act/6b4e4eb40e21c1bd02cb00a273f4d79af7c42205c1390e4e65c594ecd7a3696e
2026-02-13T19:36:27.6387088Z Unable to pull refs/heads/v4: worktree contains unstaged changes
2026-02-13T19:36:27.6387328Z Cloned https://github.com/actions/cache to /root/.cache/act/6b4e4eb40e21c1bd02cb00a273f4d79af7c42205c1390e4e65c594ecd7a3696e
2026-02-13T19:36:27.6872705Z Checked out v4
2026-02-13T19:36:27.6982618Z evaluating expression ''
2026-02-13T19:36:27.6982901Z expression '' evaluated to 'true'
2026-02-13T19:36:27.6982997Z ⭐ Run Main Checkout repository
2026-02-13T19:36:27.6983135Z Writing entry to tarball workflow/outputcmd.txt len:0
2026-02-13T19:36:27.6983269Z Writing entry to tarball workflow/statecmd.txt len:0
2026-02-13T19:36:27.6983352Z Writing entry to tarball workflow/pathcmd.txt len:0
2026-02-13T19:36:27.6983428Z Writing entry to tarball workflow/envs.txt len:0
2026-02-13T19:36:27.6983499Z Writing entry to tarball workflow/SUMMARY.md len:0
2026-02-13T19:36:27.6983583Z Extracting content to '/var/run/act'
2026-02-13T19:36:27.7024234Z expression '${{ github.repository }}' rewritten to 'format('{0}', github.repository)'
2026-02-13T19:36:27.7024368Z evaluating expression 'format('{0}', github.repository)'
2026-02-13T19:36:27.7024592Z expression 'format('{0}', github.repository)' evaluated to '%!t(string=Moyettes/DiscordClone)'
2026-02-13T19:36:27.7024727Z expression '${{ github.token }}' rewritten to 'format('{0}', github.token)'
2026-02-13T19:36:27.7024799Z evaluating expression 'format('{0}', github.token)'
2026-02-13T19:36:27.7024937Z expression 'format('{0}', github.token)' evaluated to '%!t(string=***)'
2026-02-13T19:36:27.7025098Z type=remote-action actionDir=/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab actionPath= workdir=/workspace/Moyettes/DiscordClone actionCacheDir=/root/.cache/act actionName=c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab containerActionDir=/var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T19:36:27.7025227Z /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T19:36:27.7025355Z Removing /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/.gitignore before docker cp
2026-02-13T19:36:27.7025861Z 🐳 docker cp src=/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/ dst=/var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/
2026-02-13T19:36:27.7026458Z Writing tarball /tmp/act1301575211 from /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/
2026-02-13T19:36:27.7026563Z Stripping prefix:/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/ src:/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/
2026-02-13T19:36:27.7335315Z Extracting content from '/tmp/act1301575211' to '/var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/'
2026-02-13T19:36:27.7729624Z executing remote job container: [node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]
2026-02-13T19:36:27.7730025Z 🐳 docker exec cmd=[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js] user= workdir=
2026-02-13T19:36:27.7730107Z Exec command '[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]'
2026-02-13T19:36:27.7730345Z Working directory '/workspace/Moyettes/DiscordClone'
2026-02-13T19:36:27.8726078Z ::add-matcher::/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/problem-matcher.json
2026-02-13T19:36:27.8726199Z ::add-matcher::/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/problem-matcher.json
2026-02-13T19:36:27.8727713Z Syncing repository: Moyettes/DiscordClone
2026-02-13T19:36:27.8728862Z ::group::Getting Git version info
2026-02-13T19:36:27.8729003Z Working directory is '/workspace/Moyettes/DiscordClone'
2026-02-13T19:36:27.8741986Z [command]/usr/bin/git version
2026-02-13T19:36:27.8770737Z git version 2.34.1
2026-02-13T19:36:27.8779950Z ::endgroup::
2026-02-13T19:36:27.8784183Z Copying '/root/.gitconfig' to '/tmp/dec05d8d-0df0-4365-838a-7fca1ce0a633/.gitconfig'
2026-02-13T19:36:27.8787854Z Temporarily overriding HOME='/tmp/dec05d8d-0df0-4365-838a-7fca1ce0a633' before making global git config changes
2026-02-13T19:36:27.8787943Z Adding repository directory to the temporary git global config as a safe directory
2026-02-13T19:36:27.8790187Z [command]/usr/bin/git config --global --add safe.directory /workspace/Moyettes/DiscordClone
2026-02-13T19:36:27.8806718Z Deleting the contents of '/workspace/Moyettes/DiscordClone'
2026-02-13T19:36:27.8808457Z ::group::Initializing the repository
2026-02-13T19:36:27.8809872Z [command]/usr/bin/git init /workspace/Moyettes/DiscordClone
2026-02-13T19:36:27.8830428Z hint: Using 'master' as the name for the initial branch. This default branch name
2026-02-13T19:36:27.8830601Z hint: is subject to change. To configure the initial branch name to use in all
2026-02-13T19:36:27.8830712Z hint: of your new repositories, which will suppress this warning, call:
2026-02-13T19:36:27.8830801Z hint:
2026-02-13T19:36:27.8830872Z hint: git config --global init.defaultBranch <name>
2026-02-13T19:36:27.8830951Z hint:
2026-02-13T19:36:27.8831017Z hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
2026-02-13T19:36:27.8831103Z hint: 'development'. The just-created branch can be renamed via this command:
2026-02-13T19:36:27.8831175Z hint:
2026-02-13T19:36:27.8831236Z hint: git branch -m <name>
2026-02-13T19:36:27.8834743Z Initialized empty Git repository in /workspace/Moyettes/DiscordClone/.git/
2026-02-13T19:36:27.8841310Z [command]/usr/bin/git remote add origin http://192.168.125.15:4000/Moyettes/DiscordClone
2026-02-13T19:36:27.8860768Z ::endgroup::
2026-02-13T19:36:27.8860944Z ::group::Disabling automatic garbage collection
2026-02-13T19:36:27.8862545Z [command]/usr/bin/git config --local gc.auto 0
2026-02-13T19:36:27.8880484Z ::endgroup::
2026-02-13T19:36:27.8880684Z ::group::Setting up auth
2026-02-13T19:36:27.8883759Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
2026-02-13T19:36:27.8902433Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
2026-02-13T19:36:27.9066940Z [command]/usr/bin/git config --local --name-only --get-regexp http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader
2026-02-13T19:36:27.9083692Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader' && git config --local --unset-all 'http.http://192.168.125.15:4000/.extraheader' || :"
2026-02-13T19:36:27.9237344Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
2026-02-13T19:36:27.9252826Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
2026-02-13T19:36:27.9395597Z [command]/usr/bin/git config --local http.http://192.168.125.15:4000/.extraheader AUTHORIZATION: basic ***
2026-02-13T19:36:27.9412863Z ::endgroup::
2026-02-13T19:36:27.9412967Z ::group::Fetching the repository
2026-02-13T19:36:27.9416326Z [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +6177ee9a6f3b23aa93a6e77c2705bf6761adef1e:refs/remotes/origin/main
2026-02-13T19:36:30.2694276Z From http://192.168.125.15:4000/Moyettes/DiscordClone
2026-02-13T19:36:30.2694572Z * [new ref] 6177ee9a6f3b23aa93a6e77c2705bf6761adef1e -> origin/main
2026-02-13T19:36:30.2708466Z ::endgroup::
2026-02-13T19:36:30.2708567Z ::group::Determining the checkout info
2026-02-13T19:36:30.2709664Z ::endgroup::
2026-02-13T19:36:30.2717085Z [command]/usr/bin/git sparse-checkout disable
2026-02-13T19:36:30.2740804Z [command]/usr/bin/git config --local --unset-all extensions.worktreeConfig
2026-02-13T19:36:30.2756475Z ::group::Checking out the ref
2026-02-13T19:36:30.2758337Z [command]/usr/bin/git checkout --progress --force -B main refs/remotes/origin/main
2026-02-13T19:36:30.3636484Z Switched to a new branch 'main'
2026-02-13T19:36:30.3636833Z Branch 'main' set up to track remote branch 'main' from 'origin'.
2026-02-13T19:36:30.3642228Z ::endgroup::
2026-02-13T19:36:30.3661996Z [command]/usr/bin/git log -1 --format=%H
2026-02-13T19:36:30.3679600Z 6177ee9a6f3b23aa93a6e77c2705bf6761adef1e
2026-02-13T19:36:30.3685003Z ::remove-matcher owner=checkout-git::
2026-02-13T19:36:31.3571318Z (node:219) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
2026-02-13T19:36:31.3571459Z (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-13T19:36:51.3783300Z ::warning::Failed to restore: getCacheEntry failed: Request timeout: /_apis/artifactcache/cache?keys=npm-electron-3bfeb88fe20d0eab8d1e9deb0c16af4cc320445b15ebe7f6fa7d2e30a7316852%252Cnpm-electron-&version=f2531268ab9c19c75ce7b3eb23cc11c7f69fd3cf796834d4881591e430a373ff
2026-02-13T19:36:51.3783886Z Cache not found for input keys: npm-electron-3bfeb88fe20d0eab8d1e9deb0c16af4cc320445b15ebe7f6fa7d2e30a7316852, npm-electron-
2026-02-13T19:36:52.1516780Z npm warn deprecated tar@6.2.1: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T19:36:52.3747573Z npm warn deprecated npmlog@6.0.2: This package is no longer supported.
2026-02-13T19:36:52.6982265Z npm warn deprecated lodash.isequal@4.5.0: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
2026-02-13T19:36:52.7192386Z npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
2026-02-13T19:36:52.9058437Z npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T19:36:52.9256680Z npm warn deprecated gauge@4.0.4: This package is no longer supported.
2026-02-13T19:37:51.9892421Z npm warn deprecated are-we-there-yet@3.0.1: This package is no longer supported.
2026-02-13T19:37:51.9917819Z npm warn deprecated @npmcli/move-file@2.0.1: This functionality has been moved to @npmcli/fs
2026-02-13T19:37:52.0063818Z npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T19:37:52.0202588Z npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
2026-02-13T19:37:52.0213733Z npm warn deprecated glob@8.1.0: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T19:37:52.0382534Z npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
2026-02-13T19:37:52.0383042Z npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
2026-02-13T19:37:52.0662259Z npm warn deprecated boolean@3.2.0: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
2026-02-13T19:37:52.3065967Z npm warn deprecated glob@9.3.5: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T19:37:52.3200651Z npm warn deprecated glob@10.5.0: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T19:38:01.1610842Z
2026-02-13T19:38:01.1611132Z added 677 packages, and audited 683 packages in 1m
2026-02-13T19:38:01.1611198Z
2026-02-13T19:38:01.1611239Z 183 packages are looking for funding
2026-02-13T19:38:01.1611307Z run `npm fund` for details
2026-02-13T19:38:01.1802390Z
2026-02-13T19:38:01.1802544Z 11 vulnerabilities (1 moderate, 10 high)
2026-02-13T19:38:01.1802633Z
2026-02-13T19:38:01.1802695Z To address issues that do not require attention, run:
2026-02-13T19:38:01.1802758Z npm audit fix
2026-02-13T19:38:01.1802806Z
2026-02-13T19:38:01.1802850Z To address all issues (including breaking changes), run:
2026-02-13T19:38:01.1802907Z npm audit fix --force
2026-02-13T19:38:01.1802959Z
2026-02-13T19:38:01.1803057Z Run `npm audit` for details.
2026-02-13T19:38:01.1988663Z --- app-builder diagnostics ---
2026-02-13T19:38:01.2208484Z -rwxr-xr-x 1 root root 18116608 Feb 13 19:37 node_modules/app-builder-bin/linux/x64/app-builder
2026-02-13T19:38:01.2209297Z /var/run/act/workflow/3.sh: 7: file: not found
2026-02-13T19:38:01.2215582Z ❌ Failure - Main Install dependencies
2026-02-13T19:38:01.2234947Z exitcode '127': command not found, please refer to https://github.com/nektos/act/issues/107 for more information
2026-02-13T19:38:01.2731112Z expression 'npm-electron-${{ hashFiles('package-lock.json') }}' rewritten to 'format('npm-electron-{0}', hashFiles('package-lock.json'))'
2026-02-13T19:38:01.2731336Z evaluating expression 'format('npm-electron-{0}', hashFiles('package-lock.json'))'
2026-02-13T19:38:01.2731539Z Writing entry to tarball workflow/hashfiles/index.js len:168437
2026-02-13T19:38:01.2733153Z Extracting content to '/var/run/act'
2026-02-13T19:38:01.2744106Z 🐳 docker exec cmd=[node /var/run/act/workflow/hashfiles/index.js] user= workdir=
2026-02-13T19:38:01.2744234Z Exec command '[node /var/run/act/workflow/hashfiles/index.js]'
2026-02-13T19:38:01.2744324Z Working directory '/workspace/Moyettes/DiscordClone'
2026-02-13T19:38:01.3474202Z expression 'format('npm-electron-{0}', hashFiles('package-lock.json'))' evaluated to '%!t(string=npm-electron-3bfeb88fe20d0eab8d1e9deb0c16af4cc320445b15ebe7f6fa7d2e30a7316852)'
2026-02-13T19:38:01.3509253Z evaluating expression 'success()'
2026-02-13T19:38:01.3509499Z expression 'success()' evaluated to 'false'
2026-02-13T19:38:01.3509585Z Skipping step 'Cache npm and Electron' due to 'success()'
2026-02-13T19:38:01.3599133Z evaluating expression 'always()'
2026-02-13T19:38:01.3599308Z expression 'always()' evaluated to 'true'
2026-02-13T19:38:01.3599374Z ⭐ Run Post Checkout repository
2026-02-13T19:38:01.3599464Z Writing entry to tarball workflow/outputcmd.txt len:0
2026-02-13T19:38:01.3599559Z Writing entry to tarball workflow/statecmd.txt len:0
2026-02-13T19:38:01.3599605Z Writing entry to tarball workflow/pathcmd.txt len:0
2026-02-13T19:38:01.3599656Z Writing entry to tarball workflow/envs.txt len:0
2026-02-13T19:38:01.3599696Z Writing entry to tarball workflow/SUMMARY.md len:0
2026-02-13T19:38:01.3599748Z Extracting content to '/var/run/act'
2026-02-13T19:38:01.3606678Z run post step for 'Checkout repository'
2026-02-13T19:38:01.3607073Z executing remote job container: [node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]
2026-02-13T19:38:01.3607202Z 🐳 docker exec cmd=[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js] user= workdir=
2026-02-13T19:38:01.3607317Z Exec command '[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]'
2026-02-13T19:38:01.3607538Z Working directory '/workspace/Moyettes/DiscordClone'
2026-02-13T19:38:01.4605898Z [command]/usr/bin/git version
2026-02-13T19:38:01.4797399Z git version 2.34.1
2026-02-13T19:38:01.4807817Z ***
2026-02-13T19:38:01.4812586Z Copying '/root/.gitconfig' to '/tmp/fea237a6-cf9e-4f7c-a2fa-eeee022d1604/.gitconfig'
2026-02-13T19:38:01.4817182Z Temporarily overriding HOME='/tmp/fea237a6-cf9e-4f7c-a2fa-eeee022d1604' before making global git config changes
2026-02-13T19:38:01.4817962Z Adding repository directory to the temporary git global config as a safe directory
2026-02-13T19:38:01.4820703Z [command]/usr/bin/git config --global --add safe.directory /workspace/Moyettes/DiscordClone
2026-02-13T19:38:01.4839836Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
2026-02-13T19:38:01.4862009Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
2026-02-13T19:38:01.5029359Z [command]/usr/bin/git config --local --name-only --get-regexp http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader
2026-02-13T19:38:01.5041405Z http.http://192.168.125.15:4000/.extraheader
2026-02-13T19:38:01.5048350Z [command]/usr/bin/git config --local --unset-all http.http://192.168.125.15:4000/.extraheader
2026-02-13T19:38:01.5067223Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader' && git config --local --unset-all 'http.http://192.168.125.15:4000/.extraheader' || :"
2026-02-13T19:38:01.5281659Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
2026-02-13T19:38:01.5304472Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
2026-02-13T19:38:01.5503190Z ✅ Success - Post Checkout repository
2026-02-13T19:38:01.5523214Z Cleaning up container for job build-and-release

View File

@@ -1,610 +0,0 @@
2026-02-13T19:43:21.8590060Z e0b0e1a10f48(version:v0.2.13) received task 46 of job build-and-release, be triggered by event: push
2026-02-13T19:43:21.8591995Z workflow prepared
2026-02-13T19:43:21.8592469Z evaluating expression 'success()'
2026-02-13T19:43:21.8592844Z expression 'success()' evaluated to 'true'
2026-02-13T19:43:21.8592937Z 🚀 Start image=moyettes/eb
2026-02-13T19:43:21.8634719Z 🐳 docker pull image=moyettes/eb platform= username= forcePull=false
2026-02-13T19:43:21.8634846Z 🐳 docker pull moyettes/eb
2026-02-13T19:43:21.8639116Z Image exists? true
2026-02-13T19:43:21.8703078Z Cleaning up network for job build-and-release, and network name is: GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network
2026-02-13T19:43:22.4682212Z 🐳 docker create image=moyettes/eb platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network"
2026-02-13T19:43:22.4769484Z Custom container.Config from options ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image: Volumes:map[] WorkingDir: Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-02-13T19:43:22.4769714Z Merged container.Config ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=X64 RUNNER_TEMP=/tmp LANG=C.UTF-8] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image:moyettes/eb Volumes:map[] WorkingDir:/workspace/Moyettes/DiscordClone Entrypoint:[/bin/sleep 10800] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-02-13T19:43:22.4769947Z Custom container.HostConfig from options ==> &{Binds:[] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:false VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0xc0001fa3c8 OomKillDisable:0xc0001fa2c3 PidsLimit:0xc0001fa428 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-02-13T19:43:22.4770164Z --network and --net in the options will be ignored.
2026-02-13T19:43:22.4770313Z Merged container.HostConfig ==> &{Binds:[/var/run/docker.sock:/var/run/docker.sock] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:true VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0xc0001fa3c8 OomKillDisable:0xc0001fa2c3 PidsLimit:0xc0001fa428 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[{Type:volume Source:act-toolcache Target:/opt/hostedtoolcache ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release-env Target:/var/run/act ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release Target:/workspace/Moyettes/DiscordClone ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>}] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-02-13T19:43:23.6763541Z Created container name=GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release id=78088ba67b9e9a8de8380d20c89ea44da62bb5887fd0db970d18137ccf97f44f from image moyettes/eb (platform: )
2026-02-13T19:43:23.6763911Z ENV ==> [RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=X64 RUNNER_TEMP=/tmp LANG=C.UTF-8]
2026-02-13T19:43:23.6764002Z 🐳 docker run image=moyettes/eb platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network"
2026-02-13T19:43:23.6764088Z Starting container: 78088ba67b9e9a8de8380d20c89ea44da62bb5887fd0db970d18137ccf97f44f
2026-02-13T19:43:26.4422897Z Started container: 78088ba67b9e9a8de8380d20c89ea44da62bb5887fd0db970d18137ccf97f44f
2026-02-13T19:43:26.5303431Z Writing entry to tarball workflow/event.json len:5022
2026-02-13T19:43:26.5303827Z Writing entry to tarball workflow/envs.txt len:0
2026-02-13T19:43:26.5303940Z Extracting content to '/var/run/act/'
2026-02-13T19:43:26.5380347Z ☁ git clone 'https://github.com/actions/checkout' # ref=v4
2026-02-13T19:43:26.5380532Z cloning https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T19:43:26.9946388Z Unable to pull refs/heads/v4: non-fast-forward update
2026-02-13T19:43:26.9946600Z Cloned https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T19:43:27.0057995Z Checked out v4
2026-02-13T19:43:27.0093828Z ☁ git clone 'https://github.com/actions/cache' # ref=v4
2026-02-13T19:43:27.0093941Z cloning https://github.com/actions/cache to /root/.cache/act/6b4e4eb40e21c1bd02cb00a273f4d79af7c42205c1390e4e65c594ecd7a3696e
2026-02-13T19:43:27.2377963Z Unable to pull refs/heads/v4: worktree contains unstaged changes
2026-02-13T19:43:27.2378194Z Cloned https://github.com/actions/cache to /root/.cache/act/6b4e4eb40e21c1bd02cb00a273f4d79af7c42205c1390e4e65c594ecd7a3696e
2026-02-13T19:43:27.2850405Z Checked out v4
2026-02-13T19:43:27.2948868Z evaluating expression ''
2026-02-13T19:43:27.2949102Z expression '' evaluated to 'true'
2026-02-13T19:43:27.2949163Z ⭐ Run Main Checkout repository
2026-02-13T19:43:27.2949252Z Writing entry to tarball workflow/outputcmd.txt len:0
2026-02-13T19:43:27.2949341Z Writing entry to tarball workflow/statecmd.txt len:0
2026-02-13T19:43:27.2949393Z Writing entry to tarball workflow/pathcmd.txt len:0
2026-02-13T19:43:27.2949447Z Writing entry to tarball workflow/envs.txt len:0
2026-02-13T19:43:27.2949489Z Writing entry to tarball workflow/SUMMARY.md len:0
2026-02-13T19:43:27.2949538Z Extracting content to '/var/run/act'
2026-02-13T19:43:27.2984878Z expression '${{ github.repository }}' rewritten to 'format('{0}', github.repository)'
2026-02-13T19:43:27.2985016Z evaluating expression 'format('{0}', github.repository)'
2026-02-13T19:43:27.2985256Z expression 'format('{0}', github.repository)' evaluated to '%!t(string=Moyettes/DiscordClone)'
2026-02-13T19:43:27.2985446Z expression '${{ github.token }}' rewritten to 'format('{0}', github.token)'
2026-02-13T19:43:27.2985520Z evaluating expression 'format('{0}', github.token)'
2026-02-13T19:43:27.2985666Z expression 'format('{0}', github.token)' evaluated to '%!t(string=***)'
2026-02-13T19:43:27.2985780Z type=remote-action actionDir=/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab actionPath= workdir=/workspace/Moyettes/DiscordClone actionCacheDir=/root/.cache/act actionName=c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab containerActionDir=/var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T19:43:27.2985902Z /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-02-13T19:43:27.2986028Z Removing /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/.gitignore before docker cp
2026-02-13T19:43:27.2986530Z 🐳 docker cp src=/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/ dst=/var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/
2026-02-13T19:43:27.2987122Z Writing tarball /tmp/act1670143716 from /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/
2026-02-13T19:43:27.2987227Z Stripping prefix:/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/ src:/root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/
2026-02-13T19:43:27.3302843Z Extracting content from '/tmp/act1670143716' to '/var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/'
2026-02-13T19:43:27.3688387Z executing remote job container: [node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]
2026-02-13T19:43:27.3688745Z 🐳 docker exec cmd=[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js] user= workdir=
2026-02-13T19:43:27.3688828Z Exec command '[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]'
2026-02-13T19:43:27.3689035Z Working directory '/workspace/Moyettes/DiscordClone'
2026-02-13T19:43:27.5273206Z ::add-matcher::/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/problem-matcher.json
2026-02-13T19:43:27.5273290Z ::add-matcher::/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/problem-matcher.json
2026-02-13T19:43:27.5274863Z Syncing repository: Moyettes/DiscordClone
2026-02-13T19:43:27.5275939Z ::group::Getting Git version info
2026-02-13T19:43:27.5276044Z Working directory is '/workspace/Moyettes/DiscordClone'
2026-02-13T19:43:27.5288512Z [command]/usr/bin/git version
2026-02-13T19:43:27.5317462Z git version 2.34.1
2026-02-13T19:43:27.5326809Z ::endgroup::
2026-02-13T19:43:27.5331374Z Copying '/root/.gitconfig' to '/tmp/9ec818fd-d5c9-4218-937f-276257579f19/.gitconfig'
2026-02-13T19:43:27.5335515Z Temporarily overriding HOME='/tmp/9ec818fd-d5c9-4218-937f-276257579f19' before making global git config changes
2026-02-13T19:43:27.5335653Z Adding repository directory to the temporary git global config as a safe directory
2026-02-13T19:43:27.5338264Z [command]/usr/bin/git config --global --add safe.directory /workspace/Moyettes/DiscordClone
2026-02-13T19:43:27.5355876Z Deleting the contents of '/workspace/Moyettes/DiscordClone'
2026-02-13T19:43:27.5361938Z ::group::Initializing the repository
2026-02-13T19:43:27.5363825Z [command]/usr/bin/git init /workspace/Moyettes/DiscordClone
2026-02-13T19:43:27.5379848Z hint: Using 'master' as the name for the initial branch. This default branch name
2026-02-13T19:43:27.5379906Z hint: is subject to change. To configure the initial branch name to use in all
2026-02-13T19:43:27.5379951Z hint: of your new repositories, which will suppress this warning, call:
2026-02-13T19:43:27.5380005Z hint:
2026-02-13T19:43:27.5380048Z hint: git config --global init.defaultBranch <name>
2026-02-13T19:43:27.5380091Z hint:
2026-02-13T19:43:27.5380128Z hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
2026-02-13T19:43:27.5380171Z hint: 'development'. The just-created branch can be renamed via this command:
2026-02-13T19:43:27.5380214Z hint:
2026-02-13T19:43:27.5380249Z hint: git branch -m <name>
2026-02-13T19:43:27.5381753Z Initialized empty Git repository in /workspace/Moyettes/DiscordClone/.git/
2026-02-13T19:43:27.5386979Z [command]/usr/bin/git remote add origin http://192.168.125.15:4000/Moyettes/DiscordClone
2026-02-13T19:43:27.5403050Z ::endgroup::
2026-02-13T19:43:27.5403136Z ::group::Disabling automatic garbage collection
2026-02-13T19:43:27.5404977Z [command]/usr/bin/git config --local gc.auto 0
2026-02-13T19:43:27.5418736Z ::endgroup::
2026-02-13T19:43:27.5418822Z ::group::Setting up auth
2026-02-13T19:43:27.5421993Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
2026-02-13T19:43:27.5436342Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
2026-02-13T19:43:27.5945058Z [command]/usr/bin/git config --local --name-only --get-regexp http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader
2026-02-13T19:43:27.5957281Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader' && git config --local --unset-all 'http.http://192.168.125.15:4000/.extraheader' || :"
2026-02-13T19:43:27.6100789Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
2026-02-13T19:43:27.6114980Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
2026-02-13T19:43:27.6256036Z [command]/usr/bin/git config --local http.http://192.168.125.15:4000/.extraheader AUTHORIZATION: basic ***
2026-02-13T19:43:27.6273020Z ::endgroup::
2026-02-13T19:43:27.6273114Z ::group::Fetching the repository
2026-02-13T19:43:27.6276461Z [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +6220da85141b9e4c77a30bf0c803645406c1f684:refs/remotes/origin/main
2026-02-13T19:43:29.4446658Z From http://192.168.125.15:4000/Moyettes/DiscordClone
2026-02-13T19:43:29.4446955Z * [new ref] 6220da85141b9e4c77a30bf0c803645406c1f684 -> origin/main
2026-02-13T19:43:29.4460398Z ::endgroup::
2026-02-13T19:43:29.4460566Z ::group::Determining the checkout info
2026-02-13T19:43:29.4461655Z ::endgroup::
2026-02-13T19:43:29.4469324Z [command]/usr/bin/git sparse-checkout disable
2026-02-13T19:43:29.4489210Z [command]/usr/bin/git config --local --unset-all extensions.worktreeConfig
2026-02-13T19:43:29.4503888Z ::group::Checking out the ref
2026-02-13T19:43:29.4505707Z [command]/usr/bin/git checkout --progress --force -B main refs/remotes/origin/main
2026-02-13T19:43:29.5371088Z Switched to a new branch 'main'
2026-02-13T19:43:29.5371451Z Branch 'main' set up to track remote branch 'main' from 'origin'.
2026-02-13T19:43:29.5377492Z ::endgroup::
2026-02-13T19:43:29.5399138Z [command]/usr/bin/git log -1 --format=%H
2026-02-13T19:43:29.5418768Z 6220da85141b9e4c77a30bf0c803645406c1f684
2026-02-13T19:43:29.5426020Z ::remove-matcher owner=checkout-git::
2026-02-13T19:43:30.7998481Z (node:218) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
2026-02-13T19:43:30.7998567Z (Use `node --trace-deprecation ...` to show where the warning was created)
2026-02-13T19:43:50.8411974Z ::warning::Failed to restore: getCacheEntry failed: Request timeout: /_apis/artifactcache/cache?keys=npm-electron-3bfeb88fe20d0eab8d1e9deb0c16af4cc320445b15ebe7f6fa7d2e30a7316852%252Cnpm-electron-&version=f2531268ab9c19c75ce7b3eb23cc11c7f69fd3cf796834d4881591e430a373ff
2026-02-13T19:43:50.8412611Z Cache not found for input keys: npm-electron-3bfeb88fe20d0eab8d1e9deb0c16af4cc320445b15ebe7f6fa7d2e30a7316852, npm-electron-
2026-02-13T19:43:51.5402006Z npm warn deprecated tar@6.2.1: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T19:43:51.7975546Z npm warn deprecated npmlog@6.0.2: This package is no longer supported.
2026-02-13T19:43:52.0063861Z npm warn deprecated lodash.isequal@4.5.0: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
2026-02-13T19:43:52.1082618Z npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
2026-02-13T19:43:52.1773184Z npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T19:43:52.1900248Z npm warn deprecated gauge@4.0.4: This package is no longer supported.
2026-02-13T19:43:52.4885199Z npm warn deprecated boolean@3.2.0: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
2026-02-13T19:43:52.5222171Z npm warn deprecated are-we-there-yet@3.0.1: This package is no longer supported.
2026-02-13T19:43:52.9121508Z npm warn deprecated @npmcli/move-file@2.0.1: This functionality has been moved to @npmcli/fs
2026-02-13T19:43:53.4911508Z npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
2026-02-13T19:43:53.5018834Z npm warn deprecated glob@9.3.5: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T19:43:53.6085654Z npm warn deprecated glob@10.5.0: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T19:43:53.6174294Z npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
2026-02-13T19:43:53.6218292Z npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T19:43:53.6336436Z npm warn deprecated glob@8.1.0: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
2026-02-13T19:43:53.6577415Z npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
2026-02-13T19:45:44.8599059Z
2026-02-13T19:45:44.8599517Z added 677 packages, and audited 683 packages in 2m
2026-02-13T19:45:44.8599658Z
2026-02-13T19:45:44.8599704Z 183 packages are looking for funding
2026-02-13T19:45:44.8599752Z run `npm fund` for details
2026-02-13T19:45:44.8805402Z
2026-02-13T19:45:44.8805571Z 11 vulnerabilities (1 moderate, 10 high)
2026-02-13T19:45:44.8805633Z
2026-02-13T19:45:44.8805684Z To address all issues (including breaking changes), run:
2026-02-13T19:45:44.8805753Z npm audit fix --force
2026-02-13T19:45:44.8805804Z
2026-02-13T19:45:44.8805856Z Run `npm audit` for details.
2026-02-13T19:45:44.8919475Z --- app-builder diagnostics ---
2026-02-13T19:45:44.8931305Z -rwxr-xr-x 1 root root 18116608 Feb 13 19:43 node_modules/app-builder-bin/linux/x64/app-builder
2026-02-13T19:45:45.0651733Z not a dynamic executable
2026-02-13T19:45:45.0701432Z 3.5.10
2026-02-13T19:45:45.0835728Z 3.5.10
2026-02-13T19:45:45.2948318Z
2026-02-13T19:45:45.2948942Z > @discord-clone/electron@1.0.14 build
2026-02-13T19:45:45.2949056Z > vite build
2026-02-13T19:45:45.2949130Z
2026-02-13T19:45:45.4448102Z vite v7.3.1 building client environment for production...
2026-02-13T19:45:45.4794190Z transforming...
2026-02-13T19:45:45.8322429Z The glob option "as" has been deprecated in favour of "query". Please update `as: 'url'` to `query: '?url', import: 'default'`.
2026-02-13T19:45:45.8404375Z [plugin vite:esbuild] ../../packages/shared/src/components/ScreenShareModal.jsx: Duplicate key "width" in object literal
2026-02-13T19:45:45.8404602Z 71 | }}
2026-02-13T19:45:45.8404671Z 72 | >
2026-02-13T19:45:45.8404717Z 73 | ...le={{ position: 'relative', width: '100%', height: '250px', width: '450px', borderRadius: '8px', overflow: 'hidden...
2026-02-13T19:45:45.8404781Z | ^
2026-02-13T19:45:45.8404825Z 74 | {/* Thumbnail/Placeholder */}
2026-02-13T19:45:45.8404879Z 75 | {item.thumbnail ? (
2026-02-13T19:45:45.8404928Z 
2026-02-13T19:45:48.5194274Z ✓ 4636 modules transformed.
2026-02-13T19:45:49.0494364Z rendering chunks...
2026-02-13T19:45:49.0906445Z computing gzip size...
2026-02-13T19:45:49.2419084Z dist-react/index.html  0.63 kB │ gzip: 0.39 kB
2026-02-13T19:45:49.2419377Z dist-react/assets/woman_teacher-CXwESYU3.svg  4.10 kB │ gzip: 1.60 kB
2026-02-13T19:45:49.2419447Z dist-react/assets/woman_teacher_tone2-DWJ6rjnf.svg  4.10 kB │ gzip: 1.60 kB
2026-02-13T19:45:49.2419502Z dist-react/assets/woman_teacher_tone3-BvnMOsM7.svg  4.10 kB │ gzip: 1.60 kB
2026-02-13T19:45:49.2419577Z dist-react/assets/woman_teacher_tone4-C9bkU449.svg  4.10 kB │ gzip: 1.60 kB
2026-02-13T19:45:49.2419642Z dist-react/assets/bubble_tea-Cy1d5egt.svg  4.10 kB │ gzip: 1.92 kB
2026-02-13T19:45:49.2419709Z dist-react/assets/flag_gq-B3TFx5qI.svg  4.11 kB │ gzip: 1.68 kB
2026-02-13T19:45:49.2419824Z dist-react/assets/person_in_lotus_position-9VFgclqE.svg  4.11 kB │ gzip: 1.73 kB
2026-02-13T19:45:49.2419871Z dist-react/assets/person_in_lotus_position_tone1-MK18jaNb.svg  4.11 kB │ gzip: 1.73 kB
2026-02-13T19:45:49.2419917Z dist-react/assets/person_in_lotus_position_tone2-Dzm_xqT2.svg  4.11 kB │ gzip: 1.73 kB
2026-02-13T19:45:49.2419966Z dist-react/assets/person_in_lotus_position_tone4-BtjpxNGo.svg  4.11 kB │ gzip: 1.72 kB
2026-02-13T19:45:49.2420016Z dist-react/assets/person_in_lotus_position_tone3-DUxbd8tQ.svg  4.11 kB │ gzip: 1.73 kB
2026-02-13T19:45:49.2420074Z dist-react/assets/person_in_lotus_position_tone5-CinKf4VL.svg  4.11 kB │ gzip: 1.72 kB
2026-02-13T19:45:49.2420119Z dist-react/assets/man_feeding_baby_tone5-DOWKsCGX.svg  4.11 kB │ gzip: 1.83 kB
2026-02-13T19:45:49.2420169Z dist-react/assets/man_feeding_baby-BLUtslbF.svg  4.13 kB │ gzip: 1.83 kB
2026-02-13T19:45:49.2420217Z dist-react/assets/man_feeding_baby_tone1-lg4dBAV2.svg  4.13 kB │ gzip: 1.83 kB
2026-02-13T19:45:49.2420262Z dist-react/assets/man_feeding_baby_tone4-IRS8MZPe.svg  4.13 kB │ gzip: 1.83 kB
2026-02-13T19:45:49.2420308Z dist-react/assets/man_feeding_baby_tone2-BuF25R9x.svg  4.13 kB │ gzip: 1.83 kB
2026-02-13T19:45:49.2420360Z dist-react/assets/man_feeding_baby_tone3-DEYxzqY-.svg  4.13 kB │ gzip: 1.83 kB
2026-02-13T19:45:49.2420412Z dist-react/assets/woman_zombie-Cn4gQ0af.svg  4.15 kB │ gzip: 1.73 kB
2026-02-13T19:45:49.2420466Z dist-react/assets/hiking_boot-CPXD60gE.svg  4.17 kB │ gzip: 1.76 kB
2026-02-13T19:45:49.2420513Z dist-react/assets/japanese_ogre-BsqNvmIl.svg  4.22 kB │ gzip: 1.85 kB
2026-02-13T19:45:49.2420557Z dist-react/assets/woman_police_officer_tone5-CuQMgf5h.svg  4.23 kB │ gzip: 1.72 kB
2026-02-13T19:45:49.2420606Z dist-react/assets/woman_police_officer_tone1-A8sdhmvt.svg  4.25 kB │ gzip: 1.73 kB
2026-02-13T19:45:49.2420657Z dist-react/assets/woman_police_officer_tone3-DXZ2OdUI.svg  4.25 kB │ gzip: 1.73 kB
2026-02-13T19:45:49.2420703Z dist-react/assets/woman_police_officer-D6jKOTyC.svg  4.25 kB │ gzip: 1.73 kB
2026-02-13T19:45:49.2420752Z dist-react/assets/woman_police_officer_tone2-DaHNn5-D.svg  4.25 kB │ gzip: 1.73 kB
2026-02-13T19:45:49.2420799Z dist-react/assets/woman_police_officer_tone4--Oe8w2XD.svg  4.25 kB │ gzip: 1.72 kB
2026-02-13T19:45:49.2420896Z dist-react/assets/ferris_wheel-DvW0t9g3.svg  4.25 kB │ gzip: 1.19 kB
2026-02-13T19:45:49.2420941Z dist-react/assets/man_teacher_tone5-Bk9uZHaS.svg  4.26 kB │ gzip: 1.75 kB
2026-02-13T19:45:49.2421002Z dist-react/assets/man_teacher-BRypTuYs.svg  4.27 kB │ gzip: 1.75 kB
2026-02-13T19:45:49.2421058Z dist-react/assets/man_teacher_tone2-rQoIFfFz.svg  4.27 kB │ gzip: 1.75 kB
2026-02-13T19:45:49.2421117Z dist-react/assets/man_teacher_tone4-C4j4RIq3.svg  4.27 kB │ gzip: 1.74 kB
2026-02-13T19:45:49.2421172Z dist-react/assets/man_teacher_tone3-BEE8k6p5.svg  4.27 kB │ gzip: 1.75 kB
2026-02-13T19:45:49.2421225Z dist-react/assets/man_teacher_tone1-jNO2AiRD.svg  4.27 kB │ gzip: 1.75 kB
2026-02-13T19:45:49.2421282Z dist-react/assets/sari-BSRA0_R3.svg  4.27 kB │ gzip: 1.77 kB
2026-02-13T19:45:49.2421343Z dist-react/assets/gloves-BcY_RgAR.svg  4.31 kB │ gzip: 1.62 kB
2026-02-13T19:45:49.2421390Z dist-react/assets/moon_cake-BQr_VKRq.svg  4.31 kB │ gzip: 1.94 kB
2026-02-13T19:45:49.2421446Z dist-react/assets/man_zombie-D5T1AZ12.svg  4.34 kB │ gzip: 1.78 kB
2026-02-13T19:45:49.2421507Z dist-react/assets/woman_surfing_tone1-Mj4OdRWf.svg  4.35 kB │ gzip: 1.94 kB
2026-02-13T19:45:49.2421563Z dist-react/assets/woman_surfing-22i7hQgf.svg  4.38 kB │ gzip: 1.95 kB
2026-02-13T19:45:49.2421622Z dist-react/assets/woman_surfing_tone2-BcJfdMyX.svg  4.38 kB │ gzip: 1.95 kB
2026-02-13T19:45:49.2421681Z dist-react/assets/woman_surfing_tone3-D1NUtDA8.svg  4.38 kB │ gzip: 1.95 kB
2026-02-13T19:45:49.2421830Z dist-react/assets/woman_surfing_tone4-ByYdFJZp.svg  4.38 kB │ gzip: 1.95 kB
2026-02-13T19:45:49.2421883Z dist-react/assets/woman_surfing_tone5-CrN9a9WS.svg  4.38 kB │ gzip: 1.95 kB
2026-02-13T19:45:49.2421940Z dist-react/assets/person_with_probing_cane-DRcmbgmz.svg  4.38 kB │ gzip: 1.91 kB
2026-02-13T19:45:49.2421994Z dist-react/assets/person_with_probing_cane_tone2-CY2wYkQb.svg  4.38 kB │ gzip: 1.91 kB
2026-02-13T19:45:49.2422052Z dist-react/assets/person_with_probing_cane_tone1-zKjrapc7.svg  4.38 kB │ gzip: 1.90 kB
2026-02-13T19:45:49.2422097Z dist-react/assets/person_with_probing_cane_tone3-CdJAKQXv.svg  4.38 kB │ gzip: 1.91 kB
2026-02-13T19:45:49.2422142Z dist-react/assets/person_with_probing_cane_tone4-DXYc5Dlc.svg  4.38 kB │ gzip: 1.91 kB
2026-02-13T19:45:49.2422195Z dist-react/assets/person_with_probing_cane_tone5-DFAMgo57.svg  4.38 kB │ gzip: 1.91 kB
2026-02-13T19:45:49.2422248Z dist-react/assets/skier-BTSq18N5.svg  4.39 kB │ gzip: 1.75 kB
2026-02-13T19:45:49.2422303Z dist-react/assets/woman_singer_tone5-Co-5wXNK.svg  4.40 kB │ gzip: 1.84 kB
2026-02-13T19:45:49.2422357Z dist-react/assets/woman_singer_tone1-DbW2lM_k.svg  4.42 kB │ gzip: 1.85 kB
2026-02-13T19:45:49.2422404Z dist-react/assets/man_with_probing_cane_tone1-D50RTI5B.svg  4.45 kB │ gzip: 1.94 kB
2026-02-13T19:45:49.2422457Z dist-react/assets/man_with_probing_cane-BarJlRlV.svg  4.45 kB │ gzip: 1.94 kB
2026-02-13T19:45:49.2422511Z dist-react/assets/man_with_probing_cane_tone3-GUZ14VpU.svg  4.45 kB │ gzip: 1.94 kB
2026-02-13T19:45:49.2422564Z dist-react/assets/man_with_probing_cane_tone5-BCpRNGU4.svg  4.45 kB │ gzip: 1.94 kB
2026-02-13T19:45:49.2422619Z dist-react/assets/man_with_probing_cane_tone2-CIeZuoUa.svg  4.45 kB │ gzip: 1.94 kB
2026-02-13T19:45:49.2422673Z dist-react/assets/man_with_probing_cane_tone4-Bd6iA8-d.svg  4.45 kB │ gzip: 1.94 kB
2026-02-13T19:45:49.2422726Z dist-react/assets/woman_singer_tone2-Bc-xqa4S.svg  4.45 kB │ gzip: 1.85 kB
2026-02-13T19:45:49.2422782Z dist-react/assets/woman_singer-skPaDBsj.svg  4.45 kB │ gzip: 1.84 kB
2026-02-13T19:45:49.2422833Z dist-react/assets/woman_singer_tone3-CNRJeino.svg  4.45 kB │ gzip: 1.85 kB
2026-02-13T19:45:49.2422887Z dist-react/assets/woman_singer_tone4-CEfcWjkD.svg  4.45 kB │ gzip: 1.84 kB
2026-02-13T19:45:49.2422948Z dist-react/assets/flag_sz-CsAySmAn.svg  4.46 kB │ gzip: 1.66 kB
2026-02-13T19:45:49.2423005Z dist-react/assets/snowman2-CeWFCRvE.svg  4.48 kB │ gzip: 1.15 kB
2026-02-13T19:45:49.2423066Z dist-react/assets/man_surfing-fqnQ3hm1.svg  4.49 kB │ gzip: 1.97 kB
2026-02-13T19:45:49.2423118Z dist-react/assets/man_surfing_tone2-CDUKGUjg.svg  4.49 kB │ gzip: 1.97 kB
2026-02-13T19:45:49.2423171Z dist-react/assets/man_surfing_tone1-BeC3CjNB.svg  4.49 kB │ gzip: 1.96 kB
2026-02-13T19:45:49.2423232Z dist-react/assets/man_surfing_tone4-PI8ASA2j.svg  4.49 kB │ gzip: 1.97 kB
2026-02-13T19:45:49.2423280Z dist-react/assets/man_surfing_tone3-Dt-HUBR5.svg  4.49 kB │ gzip: 1.97 kB
2026-02-13T19:45:49.2423332Z dist-react/assets/man_surfing_tone5-CyvTB2HT.svg  4.49 kB │ gzip: 1.97 kB
2026-02-13T19:45:49.2423387Z dist-react/assets/motorized_wheelchair-DYMoavTH.svg  4.50 kB │ gzip: 1.63 kB
2026-02-13T19:45:49.2423440Z dist-react/assets/woman_feeding_baby_tone5-B6CmkSrw.svg  4.54 kB │ gzip: 2.01 kB
2026-02-13T19:45:49.2423498Z dist-react/assets/flag_ht-nORDdDQL.svg  4.54 kB │ gzip: 1.88 kB
2026-02-13T19:45:49.2423557Z dist-react/assets/woman_feeding_baby_tone1-BezQI8D_.svg  4.56 kB │ gzip: 2.01 kB
2026-02-13T19:45:49.2423613Z dist-react/assets/woman_feeding_baby_tone3-DlgTa1f-.svg  4.56 kB │ gzip: 2.01 kB
2026-02-13T19:45:49.2423669Z dist-react/assets/woman_feeding_baby_tone4-DVay5Top.svg  4.56 kB │ gzip: 2.01 kB
2026-02-13T19:45:49.2423813Z dist-react/assets/woman_feeding_baby-p-8aPRtV.svg  4.56 kB │ gzip: 2.01 kB
2026-02-13T19:45:49.2423868Z dist-react/assets/woman_feeding_baby_tone2-CwnQLRQK.svg  4.56 kB │ gzip: 2.01 kB
2026-02-13T19:45:49.2423924Z dist-react/assets/flag_ad-CYOJPtjR.svg  4.59 kB │ gzip: 1.79 kB
2026-02-13T19:45:49.2423976Z dist-react/assets/woman_with_probing_cane_tone1-DuB7wHWP.svg  4.60 kB │ gzip: 2.00 kB
2026-02-13T19:45:49.2424030Z dist-react/assets/woman_with_probing_cane_tone3-CeM4gv4f.svg  4.60 kB │ gzip: 2.00 kB
2026-02-13T19:45:49.2424086Z dist-react/assets/woman_with_probing_cane-DyJEphms.svg  4.60 kB │ gzip: 2.00 kB
2026-02-13T19:45:49.2424144Z dist-react/assets/woman_with_probing_cane_tone2-GEDDmrTN.svg  4.60 kB │ gzip: 2.00 kB
2026-02-13T19:45:49.2424201Z dist-react/assets/woman_with_probing_cane_tone4-BaEnBpNy.svg  4.60 kB │ gzip: 2.00 kB
2026-02-13T19:45:49.2424253Z dist-react/assets/woman_with_probing_cane_tone5-DimRSWot.svg  4.60 kB │ gzip: 2.00 kB
2026-02-13T19:45:49.2424315Z dist-react/assets/person_surfing-DEOz-TJs.svg  4.62 kB │ gzip: 2.10 kB
2026-02-13T19:45:49.2424368Z dist-react/assets/person_surfing_tone1-B_kCGm1b.svg  4.62 kB │ gzip: 2.10 kB
2026-02-13T19:45:49.2424424Z dist-react/assets/person_surfing_tone2-D35jmuhN.svg  4.62 kB │ gzip: 2.11 kB
2026-02-13T19:45:49.2424468Z dist-react/assets/person_surfing_tone3-C60a5Aj1.svg  4.62 kB │ gzip: 2.10 kB
2026-02-13T19:45:49.2424526Z dist-react/assets/person_surfing_tone4-nIpCJUpE.svg  4.62 kB │ gzip: 2.10 kB
2026-02-13T19:45:49.2424583Z dist-react/assets/person_surfing_tone5-CXXi5x8s.svg  4.62 kB │ gzip: 2.10 kB
2026-02-13T19:45:49.2424634Z dist-react/assets/flag_tc-Dn_lC0KY.svg  4.72 kB │ gzip: 1.94 kB
2026-02-13T19:45:49.2424691Z dist-react/assets/person_golfing_tone2-BbPQ5nEE.svg  4.72 kB │ gzip: 2.15 kB
2026-02-13T19:45:49.2424747Z dist-react/assets/person_golfing_tone1-DhUQwNf7.svg  4.72 kB │ gzip: 2.15 kB
2026-02-13T19:45:49.2424799Z dist-react/assets/person_golfing-Mc5PuEC7.svg  4.72 kB │ gzip: 2.15 kB
2026-02-13T19:45:49.2424850Z dist-react/assets/person_golfing_tone3-CKRqu9yJ.svg  4.72 kB │ gzip: 2.15 kB
2026-02-13T19:45:49.2424911Z dist-react/assets/person_golfing_tone4-DPEV2yNK.svg  4.72 kB │ gzip: 2.15 kB
2026-02-13T19:45:49.2424963Z dist-react/assets/person_golfing_tone5-Bgq3Ud_d.svg  4.72 kB │ gzip: 2.15 kB
2026-02-13T19:45:49.2425009Z dist-react/assets/flag_ki-Ccc3Xi24.svg  4.77 kB │ gzip: 1.66 kB
2026-02-13T19:45:49.2425059Z dist-react/assets/flag_mo-PAf1BQIO.svg  4.77 kB │ gzip: 2.24 kB
2026-02-13T19:45:49.2425111Z dist-react/assets/flag_sh-CT89bJZi.svg  4.80 kB │ gzip: 1.67 kB
2026-02-13T19:45:49.2425167Z dist-react/assets/crab-D6qU1zIW.svg  4.87 kB │ gzip: 1.97 kB
2026-02-13T19:45:49.2425220Z dist-react/assets/couple-KSrP6fk0.svg  4.90 kB │ gzip: 2.04 kB
2026-02-13T19:45:49.2425264Z dist-react/assets/kiwi-BWXr7Vjo.svg  4.91 kB │ gzip: 2.27 kB
2026-02-13T19:45:49.2425309Z dist-react/assets/hedgehog-CMNxZzfp.svg  4.92 kB │ gzip: 1.82 kB
2026-02-13T19:45:49.2425359Z dist-react/assets/money_with_wings-BnGoAiwj.svg  4.95 kB │ gzip: 2.07 kB
2026-02-13T19:45:49.2425408Z dist-react/assets/flag_kg-D_P2G_Do.svg  4.98 kB │ gzip: 2.14 kB
2026-02-13T19:45:49.2425451Z dist-react/assets/maracas-kQiWhg0J.svg  4.99 kB │ gzip: 1.76 kB
2026-02-13T19:45:49.2425497Z dist-react/assets/x_ray-CWrdKTDm.svg  4.99 kB │ gzip: 2.37 kB
2026-02-13T19:45:49.2425544Z dist-react/assets/people_holding_hands-BRZihiu5.svg  5.03 kB │ gzip: 1.68 kB
2026-02-13T19:45:49.2425596Z dist-react/assets/butterfly-AxzpD-Pg.svg  5.04 kB │ gzip: 1.86 kB
2026-02-13T19:45:49.2425643Z dist-react/assets/flag_xk-D1vfCqOd.svg  5.08 kB │ gzip: 2.15 kB
2026-02-13T19:45:49.2425688Z dist-react/assets/flag_tm-_4vioey7.svg  5.13 kB │ gzip: 1.54 kB
2026-02-13T19:45:49.2425731Z dist-react/assets/two_men_holding_hands-BKJxHZb_.svg  5.15 kB │ gzip: 1.50 kB
2026-02-13T19:45:49.2425781Z dist-react/assets/seal-Djs0F0U5.svg  5.16 kB │ gzip: 2.18 kB
2026-02-13T19:45:49.2425829Z dist-react/assets/speak_no_evil-EoRZCJhS.svg  5.20 kB │ gzip: 2.27 kB
2026-02-13T19:45:49.2425885Z dist-react/assets/man_dancing-Dg-6O6t7.svg  5.22 kB │ gzip: 2.00 kB
2026-02-13T19:45:49.2425937Z dist-react/assets/man_dancing_tone1-XI7g5maV.svg  5.22 kB │ gzip: 2.00 kB
2026-02-13T19:45:49.2426074Z dist-react/assets/man_dancing_tone2-DBvANdsI.svg  5.22 kB │ gzip: 2.00 kB
2026-02-13T19:45:49.2426128Z dist-react/assets/man_dancing_tone4-D9FZRxV5.svg  5.22 kB │ gzip: 2.00 kB
2026-02-13T19:45:49.2426176Z dist-react/assets/man_dancing_tone3-BK7ka3J3.svg  5.22 kB │ gzip: 2.00 kB
2026-02-13T19:45:49.2426227Z dist-react/assets/man_dancing_tone5-CQh9niVO.svg  5.22 kB │ gzip: 2.00 kB
2026-02-13T19:45:49.2426287Z dist-react/assets/flag_pf-OA_PTTaZ.svg  5.22 kB │ gzip: 1.81 kB
2026-02-13T19:45:49.2426339Z dist-react/assets/pie-DZ6nmSau.svg  5.24 kB │ gzip: 1.98 kB
2026-02-13T19:45:49.2426394Z dist-react/assets/two_women_holding_hands-CnKtKnaZ.svg  5.29 kB │ gzip: 1.71 kB
2026-02-13T19:45:49.2426448Z dist-react/assets/brain-Czvux5Q4.svg  5.32 kB │ gzip: 2.48 kB
2026-02-13T19:45:49.2426505Z dist-react/assets/lacrosse-DK95k1kF.svg  5.33 kB │ gzip: 2.22 kB
2026-02-13T19:45:49.2426549Z dist-react/assets/see_no_evil-DnuksPIR.svg  5.42 kB │ gzip: 2.38 kB
2026-02-13T19:45:49.2426595Z dist-react/assets/flag_vi-vzZjsoBi.svg  5.43 kB │ gzip: 2.34 kB
2026-02-13T19:45:49.2426646Z dist-react/assets/dodo-CoZFlciJ.svg  5.44 kB │ gzip: 2.31 kB
2026-02-13T19:45:49.2426694Z dist-react/assets/flag_hk-CzNuCBPg.svg  5.46 kB │ gzip: 2.52 kB
2026-02-13T19:45:49.2426816Z dist-react/assets/spider_web-DPcv-q20.svg  5.50 kB │ gzip: 2.43 kB
2026-02-13T19:45:49.2426866Z dist-react/assets/flag_bl-BoaeaHPp.svg  5.58 kB │ gzip: 1.90 kB
2026-02-13T19:45:49.2426910Z dist-react/assets/flag_as-B43i20pO.svg  5.61 kB │ gzip: 2.39 kB
2026-02-13T19:45:49.2426962Z dist-react/assets/flag_gp-DW1UVBGw.svg  5.63 kB │ gzip: 2.05 kB
2026-02-13T19:45:49.2427008Z dist-react/assets/flag_ic-BrB5Xakj.svg  5.63 kB │ gzip: 2.24 kB
2026-02-13T19:45:49.2427053Z dist-react/assets/man_golfing-DhSLN6KQ.svg  5.64 kB │ gzip: 2.61 kB
2026-02-13T19:45:49.2427097Z dist-react/assets/man_golfing_tone3-vxj0o6sI.svg  5.64 kB │ gzip: 2.61 kB
2026-02-13T19:45:49.2427147Z dist-react/assets/man_golfing_tone2-CNmTGsfk.svg  5.64 kB │ gzip: 2.61 kB
2026-02-13T19:45:49.2427202Z dist-react/assets/man_golfing_tone1-DBE1f7b1.svg  5.64 kB │ gzip: 2.61 kB
2026-02-13T19:45:49.2427253Z dist-react/assets/man_golfing_tone4-CsGYmisz.svg  5.64 kB │ gzip: 2.61 kB
2026-02-13T19:45:49.2427306Z dist-react/assets/man_golfing_tone5-Cf_z4uyg.svg  5.64 kB │ gzip: 2.60 kB
2026-02-13T19:45:49.2427361Z dist-react/assets/flag_bm-CzSakp_Z.svg  5.67 kB │ gzip: 2.37 kB
2026-02-13T19:45:49.2427425Z dist-react/assets/map-BGXvLkiw.svg  5.68 kB │ gzip: 2.62 kB
2026-02-13T19:45:49.2427478Z dist-react/assets/people_wrestling-DjCLlDDS.svg  5.71 kB │ gzip: 2.48 kB
2026-02-13T19:45:49.2427531Z dist-react/assets/tamale-2biJGrAo.svg  5.74 kB │ gzip: 2.53 kB
2026-02-13T19:45:49.2427583Z dist-react/assets/men_wrestling-BNuLmHCV.svg  5.74 kB │ gzip: 2.47 kB
2026-02-13T19:45:49.2427639Z dist-react/assets/empty_nest-DGy7reBo.svg  5.79 kB │ gzip: 2.71 kB
2026-02-13T19:45:49.2427791Z dist-react/assets/worm-CxRJMG1n.svg  5.86 kB │ gzip: 2.77 kB
2026-02-13T19:45:49.2427843Z dist-react/assets/flag_bo-B7hNQ755.svg  5.91 kB │ gzip: 2.22 kB
2026-02-13T19:45:49.2427900Z dist-react/assets/face_in_clouds-DBzCKo8S.svg  5.94 kB │ gzip: 2.63 kB
2026-02-13T19:45:49.2427948Z dist-react/assets/women_wrestling-CARP3ZvF.svg  5.96 kB │ gzip: 2.59 kB
2026-02-13T19:45:49.2427998Z dist-react/assets/man_lifting_weights_tone5-cnCH-jDP.svg  6.00 kB │ gzip: 2.06 kB
2026-02-13T19:45:49.2428053Z dist-react/assets/man_lifting_weights-DkiBT0IO.svg  6.03 kB │ gzip: 2.07 kB
2026-02-13T19:45:49.2428110Z dist-react/assets/man_lifting_weights_tone1-DGilOf2d.svg  6.03 kB │ gzip: 2.06 kB
2026-02-13T19:45:49.2428165Z dist-react/assets/man_lifting_weights_tone4-MLQqpJKZ.svg  6.03 kB │ gzip: 2.07 kB
2026-02-13T19:45:49.2428219Z dist-react/assets/man_lifting_weights_tone2-CXUv2fBp.svg  6.03 kB │ gzip: 2.07 kB
2026-02-13T19:45:49.2428276Z dist-react/assets/man_lifting_weights_tone3-DhF3q93u.svg  6.03 kB │ gzip: 2.07 kB
2026-02-13T19:45:49.2428329Z dist-react/assets/flag_fj-B2-D6gPQ.svg  6.04 kB │ gzip: 2.64 kB
2026-02-13T19:45:49.2428384Z dist-react/assets/flag_pn-Bde7vecB.svg  6.05 kB │ gzip: 2.68 kB
2026-02-13T19:45:49.2428437Z dist-react/assets/flag_bt-COHVTZ6I.svg  6.06 kB │ gzip: 2.52 kB
2026-02-13T19:45:49.2428491Z dist-react/assets/person_doing_cartwheel-B6e7BEW_.svg  6.07 kB │ gzip: 2.65 kB
2026-02-13T19:45:49.2428554Z dist-react/assets/person_doing_cartwheel_tone1-TBt_b-Oj.svg  6.07 kB │ gzip: 2.65 kB
2026-02-13T19:45:49.2428609Z dist-react/assets/person_doing_cartwheel_tone2-BR4ztGzg.svg  6.07 kB │ gzip: 2.65 kB
2026-02-13T19:45:49.2428662Z dist-react/assets/person_doing_cartwheel_tone4-j074vq-9.svg  6.07 kB │ gzip: 2.65 kB
2026-02-13T19:45:49.2428850Z dist-react/assets/person_doing_cartwheel_tone3-BzmNF0vv.svg  6.07 kB │ gzip: 2.65 kB
2026-02-13T19:45:49.2428905Z dist-react/assets/person_doing_cartwheel_tone5-BzNEt2oA.svg  6.07 kB │ gzip: 2.65 kB
2026-02-13T19:45:49.2428957Z dist-react/assets/accordion-BPueGNgN.svg  6.07 kB │ gzip: 1.20 kB
2026-02-13T19:45:49.2429012Z dist-react/assets/lobster-Cfls8jg_.svg  6.07 kB │ gzip: 2.32 kB
2026-02-13T19:45:49.2429067Z dist-react/assets/volcano-Bh_Lqk9r.svg  6.14 kB │ gzip: 2.70 kB
2026-02-13T19:45:49.2429121Z dist-react/assets/man_cartwheeling-NFQt9ZB9.svg  6.17 kB │ gzip: 2.69 kB
2026-02-13T19:45:49.2429179Z dist-react/assets/man_cartwheeling_tone1-B3S_eUE1.svg  6.17 kB │ gzip: 2.69 kB
2026-02-13T19:45:49.2429223Z dist-react/assets/man_cartwheeling_tone2-CYBBI2iM.svg  6.17 kB │ gzip: 2.69 kB
2026-02-13T19:45:49.2429270Z dist-react/assets/man_cartwheeling_tone3-D2kqEChS.svg  6.17 kB │ gzip: 2.69 kB
2026-02-13T19:45:49.2429324Z dist-react/assets/man_cartwheeling_tone5-PFLWmq7Q.svg  6.17 kB │ gzip: 2.69 kB
2026-02-13T19:45:49.2429380Z dist-react/assets/man_cartwheeling_tone4-B96D58fZ.svg  6.17 kB │ gzip: 2.69 kB
2026-02-13T19:45:49.2429436Z dist-react/assets/flag_lb-DHr4ylgr.svg  6.19 kB │ gzip: 2.75 kB
2026-02-13T19:45:49.2429489Z dist-react/assets/man_playing_handball_tone1-22QBgB92.svg  6.21 kB │ gzip: 2.62 kB
2026-02-13T19:45:49.2429544Z dist-react/assets/man_playing_handball-C_yN7fGQ.svg  6.21 kB │ gzip: 2.62 kB
2026-02-13T19:45:49.2429598Z dist-react/assets/man_playing_handball_tone2-Bs8PtV12.svg  6.21 kB │ gzip: 2.62 kB
2026-02-13T19:45:49.2429651Z dist-react/assets/man_playing_handball_tone3-q-BDso_I.svg  6.21 kB │ gzip: 2.62 kB
2026-02-13T19:45:49.2429716Z dist-react/assets/man_playing_handball_tone5-DK-UJ5SH.svg  6.21 kB │ gzip: 2.62 kB
2026-02-13T19:45:49.2429768Z dist-react/assets/man_playing_handball_tone4-BUH96fLA.svg  6.21 kB │ gzip: 2.62 kB
2026-02-13T19:45:49.2429820Z dist-react/assets/flag_fk-1KKBtSFw.svg  6.22 kB │ gzip: 2.54 kB
2026-02-13T19:45:49.2429872Z dist-react/assets/flag_rs-CmpxaRIS.svg  6.23 kB │ gzip: 2.35 kB
2026-02-13T19:45:49.2429929Z dist-react/assets/cucumber-oVkPYVB9.svg  6.24 kB │ gzip: 2.01 kB
2026-02-13T19:45:49.2429979Z dist-react/assets/woman_lifting_weights_tone5-BJQrRdVE.svg  6.25 kB │ gzip: 2.15 kB
2026-02-13T19:45:49.2430038Z dist-react/assets/woman_lifting_weights-CsixMYFL.svg  6.28 kB │ gzip: 2.15 kB
2026-02-13T19:45:49.2430092Z dist-react/assets/woman_lifting_weights_tone1-BpRsBk7z.svg  6.28 kB │ gzip: 2.15 kB
2026-02-13T19:45:49.2430147Z dist-react/assets/woman_lifting_weights_tone2-P18Nfbuz.svg  6.28 kB │ gzip: 2.15 kB
2026-02-13T19:45:49.2430211Z dist-react/assets/woman_lifting_weights_tone3-C0gnGp49.svg  6.28 kB │ gzip: 2.15 kB
2026-02-13T19:45:49.2430265Z dist-react/assets/woman_lifting_weights_tone4-CQZmiYUl.svg  6.28 kB │ gzip: 2.16 kB
2026-02-13T19:45:49.2430318Z dist-react/assets/person_lifting_weights_tone5-DEciUSJH.svg  6.29 kB │ gzip: 2.20 kB
2026-02-13T19:45:49.2430371Z dist-react/assets/person_lifting_weights-Cn0dQ6qY.svg  6.30 kB │ gzip: 2.20 kB
2026-02-13T19:45:49.2430438Z dist-react/assets/person_lifting_weights_tone1-CXfKAA0L.svg  6.30 kB │ gzip: 2.19 kB
2026-02-13T19:45:49.2430494Z dist-react/assets/person_lifting_weights_tone2-Dkw3-09P.svg  6.30 kB │ gzip: 2.20 kB
2026-02-13T19:45:49.2430548Z dist-react/assets/person_lifting_weights_tone4-C62SuN24.svg  6.30 kB │ gzip: 2.20 kB
2026-02-13T19:45:49.2430600Z dist-react/assets/person_lifting_weights_tone3-3OqiHF7e.svg  6.30 kB │ gzip: 2.20 kB
2026-02-13T19:45:49.2430653Z dist-react/assets/women_with_bunny_ears_partying-CKr9TLic.svg  6.30 kB │ gzip: 1.68 kB
2026-02-13T19:45:49.2430712Z dist-react/assets/flag_sm-BYO1ASeM.svg  6.31 kB │ gzip: 2.30 kB
2026-02-13T19:45:49.2430766Z dist-react/assets/coat-Cbu3wnI6.svg  6.35 kB │ gzip: 2.52 kB
2026-02-13T19:45:49.2430823Z dist-react/assets/woman_cartwheeling_tone1-fJFXi2hD.svg  6.37 kB │ gzip: 2.78 kB
2026-02-13T19:45:49.2430876Z dist-react/assets/woman_cartwheeling-tGvm940R.svg  6.37 kB │ gzip: 2.78 kB
2026-02-13T19:45:49.2430929Z dist-react/assets/woman_cartwheeling_tone2-C5lE2K9g.svg  6.37 kB │ gzip: 2.77 kB
2026-02-13T19:45:49.2430985Z dist-react/assets/woman_cartwheeling_tone3-BourpL3A.svg  6.37 kB │ gzip: 2.77 kB
2026-02-13T19:45:49.2431045Z dist-react/assets/woman_cartwheeling_tone4-CjyM2w54.svg  6.37 kB │ gzip: 2.77 kB
2026-02-13T19:45:49.2431098Z dist-react/assets/woman_cartwheeling_tone5-D-eW47Ua.svg  6.37 kB │ gzip: 2.77 kB
2026-02-13T19:45:49.2431150Z dist-react/assets/man_running_tone1-BbRoQah0.svg  6.37 kB │ gzip: 2.89 kB
2026-02-13T19:45:49.2431210Z dist-react/assets/man_running-Bp7fZpx0.svg  6.37 kB │ gzip: 2.89 kB
2026-02-13T19:45:49.2431264Z dist-react/assets/man_running_tone2-gBe1A9EP.svg  6.37 kB │ gzip: 2.89 kB
2026-02-13T19:45:49.2431316Z dist-react/assets/man_running_tone4-CeeXJkX_.svg  6.37 kB │ gzip: 2.88 kB
2026-02-13T19:45:49.2431369Z dist-react/assets/man_running_tone5-Do-aIXEX.svg  6.37 kB │ gzip: 2.88 kB
2026-02-13T19:45:49.2431422Z dist-react/assets/man_running_tone3-DfAx9qZO.svg  6.37 kB │ gzip: 2.88 kB
2026-02-13T19:45:49.2431478Z dist-react/assets/woman_playing_handball-fiyPmBDz.svg  6.42 kB │ gzip: 2.73 kB
2026-02-13T19:45:49.2431539Z dist-react/assets/woman_playing_handball_tone2-BtTxnxhZ.svg  6.42 kB │ gzip: 2.73 kB
2026-02-13T19:45:49.2431591Z dist-react/assets/woman_playing_handball_tone1-B_P42W0r.svg  6.42 kB │ gzip: 2.73 kB
2026-02-13T19:45:49.2431644Z dist-react/assets/woman_playing_handball_tone3-C7TXAAWV.svg  6.42 kB │ gzip: 2.72 kB
2026-02-13T19:45:49.2431821Z dist-react/assets/woman_playing_handball_tone4-CtCwRGCv.svg  6.42 kB │ gzip: 2.73 kB
2026-02-13T19:45:49.2431880Z dist-react/assets/woman_playing_handball_tone5-CmZlugee.svg  6.42 kB │ gzip: 2.73 kB
2026-02-13T19:45:49.2432108Z dist-react/assets/microbe-DHWlm4x3.svg  6.48 kB │ gzip: 2.82 kB
2026-02-13T19:45:49.2432162Z dist-react/assets/horse_racing_tone2-kHM6lt0G.svg  6.50 kB │ gzip: 2.88 kB
2026-02-13T19:45:49.2432217Z dist-react/assets/horse_racing-Cd5KXigQ.svg  6.50 kB │ gzip: 2.87 kB
2026-02-13T19:45:49.2432273Z dist-react/assets/horse_racing_tone1-BPFu29EM.svg  6.50 kB │ gzip: 2.87 kB
2026-02-13T19:45:49.2432328Z dist-react/assets/horse_racing_tone3-1prjoMK9.svg  6.50 kB │ gzip: 2.88 kB
2026-02-13T19:45:49.2432395Z dist-react/assets/horse_racing_tone4-DZVx5-VD.svg  6.50 kB │ gzip: 2.87 kB
2026-02-13T19:45:49.2432449Z dist-react/assets/horse_racing_tone5-DoKtvypB.svg  6.50 kB │ gzip: 2.88 kB
2026-02-13T19:45:49.2432502Z dist-react/assets/person_in_manual_wheelchair-B2ofcHYu.svg  6.50 kB │ gzip: 2.57 kB
2026-02-13T19:45:49.2432565Z dist-react/assets/person_in_manual_wheelchair_tone2-DmJ1Zffk.svg  6.50 kB │ gzip: 2.57 kB
2026-02-13T19:45:49.2432622Z dist-react/assets/person_in_manual_wheelchair_tone1-BrR0l2XR.svg  6.50 kB │ gzip: 2.57 kB
2026-02-13T19:45:49.2432675Z dist-react/assets/person_in_manual_wheelchair_tone4-TZTDWyKD.svg  6.50 kB │ gzip: 2.57 kB
2026-02-13T19:45:49.2432728Z dist-react/assets/person_in_manual_wheelchair_tone3-Bt_5AaRy.svg  6.50 kB │ gzip: 2.57 kB
2026-02-13T19:45:49.2432783Z dist-react/assets/person_in_manual_wheelchair_tone5-DrOKlCDl.svg  6.50 kB │ gzip: 2.57 kB
2026-02-13T19:45:49.2432838Z dist-react/assets/burrito-B4L0kbwK.svg  6.52 kB │ gzip: 2.58 kB
2026-02-13T19:45:49.2432890Z dist-react/assets/person_running_tone3-Dist2leS.svg  6.52 kB │ gzip: 2.94 kB
2026-02-13T19:45:49.2432945Z dist-react/assets/person_running_tone2-DNzEDUb0.svg  6.52 kB │ gzip: 2.94 kB
2026-02-13T19:45:49.2432996Z dist-react/assets/person_running-DNDUEkxU.svg  6.52 kB │ gzip: 2.94 kB
2026-02-13T19:45:49.2433050Z dist-react/assets/person_running_tone1-B8sLRwke.svg  6.52 kB │ gzip: 2.94 kB
2026-02-13T19:45:49.2433109Z dist-react/assets/person_running_tone5-DEOJVy8u.svg  6.52 kB │ gzip: 2.93 kB
2026-02-13T19:45:49.2433161Z dist-react/assets/person_running_tone4-DVBWC3-p.svg  6.52 kB │ gzip: 2.94 kB
2026-02-13T19:45:49.2433213Z dist-react/assets/man_in_manual_wheelchair_tone1-Da2hybrT.svg  6.57 kB │ gzip: 2.62 kB
2026-02-13T19:45:49.2433270Z dist-react/assets/man_in_manual_wheelchair-cGfKOLRc.svg  6.60 kB │ gzip: 2.63 kB
2026-02-13T19:45:49.2433326Z dist-react/assets/man_in_manual_wheelchair_tone5-YZQTD5Nr.svg  6.60 kB │ gzip: 2.63 kB
2026-02-13T19:45:49.2433380Z dist-react/assets/man_in_manual_wheelchair_tone2-BPBmkRcs.svg  6.60 kB │ gzip: 2.63 kB
2026-02-13T19:45:49.2433433Z dist-react/assets/man_in_manual_wheelchair_tone3-H5kpv3q_.svg  6.60 kB │ gzip: 2.63 kB
2026-02-13T19:45:49.2433488Z dist-react/assets/man_in_manual_wheelchair_tone4-BvKWPBcq.svg  6.60 kB │ gzip: 2.63 kB
2026-02-13T19:45:49.2433541Z dist-react/assets/person_playing_handball-CH3hWpQR.svg  6.62 kB │ gzip: 2.66 kB
2026-02-13T19:45:49.2433599Z dist-react/assets/person_playing_handball_tone1-CbOONp_g.svg  6.62 kB │ gzip: 2.66 kB
2026-02-13T19:45:49.2433655Z dist-react/assets/person_playing_handball_tone2-jeC51_-P.svg  6.62 kB │ gzip: 2.66 kB
2026-02-13T19:45:49.2433710Z dist-react/assets/person_playing_handball_tone3-BGgWTsuS.svg  6.62 kB │ gzip: 2.66 kB
2026-02-13T19:45:49.2433763Z dist-react/assets/person_playing_handball_tone4-BsA09Avm.svg  6.62 kB │ gzip: 2.66 kB
2026-02-13T19:45:49.2433817Z dist-react/assets/person_playing_handball_tone5-D_rmeJiN.svg  6.62 kB │ gzip: 2.66 kB
2026-02-13T19:45:49.2433870Z dist-react/assets/men_with_bunny_ears_partying-DabknRQ1.svg  6.64 kB │ gzip: 1.78 kB
2026-02-13T19:45:49.2433923Z dist-react/assets/man_bouncing_ball-BCtAjpGP.svg  6.67 kB │ gzip: 3.04 kB
2026-02-13T19:45:49.2433975Z dist-react/assets/man_bouncing_ball_tone2-pU3f7Oqo.svg  6.67 kB │ gzip: 3.05 kB
2026-02-13T19:45:49.2434029Z dist-react/assets/man_bouncing_ball_tone1-BrCW39oq.svg  6.67 kB │ gzip: 3.05 kB
2026-02-13T19:45:49.2434076Z dist-react/assets/man_bouncing_ball_tone4-BonEB_V5.svg  6.67 kB │ gzip: 3.05 kB
2026-02-13T19:45:49.2434121Z dist-react/assets/man_bouncing_ball_tone3-CMYhYDFZ.svg  6.67 kB │ gzip: 3.05 kB
2026-02-13T19:45:49.2434175Z dist-react/assets/man_bouncing_ball_tone5-mVU7qtFm.svg  6.67 kB │ gzip: 3.05 kB
2026-02-13T19:45:49.2434228Z dist-react/assets/woman_running-_mwbLWM0.svg  6.74 kB │ gzip: 3.03 kB
2026-02-13T19:45:49.2434280Z dist-react/assets/woman_running_tone1-Dfqdg043.svg  6.74 kB │ gzip: 3.03 kB
2026-02-13T19:45:49.2434339Z dist-react/assets/woman_running_tone2-rXRqTMa0.svg  6.74 kB │ gzip: 3.03 kB
2026-02-13T19:45:49.2434392Z dist-react/assets/woman_running_tone3-BmRDPwCM.svg  6.74 kB │ gzip: 3.03 kB
2026-02-13T19:45:49.2434446Z dist-react/assets/woman_running_tone4-DmFzAsxD.svg  6.74 kB │ gzip: 3.03 kB
2026-02-13T19:45:49.2434498Z dist-react/assets/woman_running_tone5-C66GYSAh.svg  6.74 kB │ gzip: 3.03 kB
2026-02-13T19:45:49.2434552Z dist-react/assets/woman_in_manual_wheelchair-Ba72kfnU.svg  6.75 kB │ gzip: 2.66 kB
2026-02-13T19:45:49.2434606Z dist-react/assets/woman_in_manual_wheelchair_tone1-Ce9x88Rf.svg  6.75 kB │ gzip: 2.66 kB
2026-02-13T19:45:49.2434659Z dist-react/assets/woman_in_manual_wheelchair_tone2-CAKIPnJE.svg  6.75 kB │ gzip: 2.66 kB
2026-02-13T19:45:49.2434718Z dist-react/assets/woman_in_manual_wheelchair_tone3-D4YsEoBp.svg  6.75 kB │ gzip: 2.66 kB
2026-02-13T19:45:49.2434775Z dist-react/assets/woman_in_manual_wheelchair_tone4-BD3k04p2.svg  6.75 kB │ gzip: 2.66 kB
2026-02-13T19:45:49.2434888Z dist-react/assets/woman_in_manual_wheelchair_tone5-BmBeJ4-f.svg  6.75 kB │ gzip: 2.66 kB
2026-02-13T19:45:49.2434943Z dist-react/assets/person_in_motorized_wheelchair_tone1-Dcta4qUb.svg  6.80 kB │ gzip: 2.68 kB
2026-02-13T19:45:49.2434998Z dist-react/assets/person_in_motorized_wheelchair_tone2-C8UQYonN.svg  6.80 kB │ gzip: 2.69 kB
2026-02-13T19:45:49.2435052Z dist-react/assets/person_in_motorized_wheelchair-DxhhvjYe.svg  6.80 kB │ gzip: 2.69 kB
2026-02-13T19:45:49.2435108Z dist-react/assets/person_in_motorized_wheelchair_tone4-DLSO0rlF.svg  6.80 kB │ gzip: 2.69 kB
2026-02-13T19:45:49.2435164Z dist-react/assets/person_in_motorized_wheelchair_tone5-SnULyxgF.svg  6.80 kB │ gzip: 2.69 kB
2026-02-13T19:45:49.2435223Z dist-react/assets/person_in_motorized_wheelchair_tone3-BRD_Obbg.svg  6.80 kB │ gzip: 2.69 kB
2026-02-13T19:45:49.2435277Z dist-react/assets/person_bouncing_ball-H1IsbPT2.svg  6.82 kB │ gzip: 3.10 kB
2026-02-13T19:45:49.2435330Z dist-react/assets/person_bouncing_ball_tone1-BIhBY2_P.svg  6.82 kB │ gzip: 3.10 kB
2026-02-13T19:45:49.2435387Z dist-react/assets/person_bouncing_ball_tone2-9V5mlEG0.svg  6.82 kB │ gzip: 3.10 kB
2026-02-13T19:45:49.2435438Z dist-react/assets/person_bouncing_ball_tone5-C9pS5gcg.svg  6.82 kB │ gzip: 3.10 kB
2026-02-13T19:45:49.2435491Z dist-react/assets/person_bouncing_ball_tone4-BycyNnMy.svg  6.82 kB │ gzip: 3.10 kB
2026-02-13T19:45:49.2435542Z dist-react/assets/person_bouncing_ball_tone3-DSpJYpZ1.svg  6.82 kB │ gzip: 3.10 kB
2026-02-13T19:45:49.2435594Z dist-react/assets/man_in_motorized_wheelchair_tone1-B-J_H3TB.svg  6.85 kB │ gzip: 2.71 kB
2026-02-13T19:45:49.2435651Z dist-react/assets/flag_ms-BKjfidu-.svg  6.86 kB │ gzip: 3.00 kB
2026-02-13T19:45:49.2435702Z dist-react/assets/man_in_motorized_wheelchair-CiMQlH-Z.svg  6.87 kB │ gzip: 2.72 kB
2026-02-13T19:45:49.2435755Z dist-react/assets/man_in_motorized_wheelchair_tone2-DQy0C3Cx.svg  6.87 kB │ gzip: 2.72 kB
2026-02-13T19:45:49.2435810Z dist-react/assets/man_in_motorized_wheelchair_tone4-CoEn9n-F.svg  6.87 kB │ gzip: 2.72 kB
2026-02-13T19:45:49.2435865Z dist-react/assets/man_in_motorized_wheelchair_tone3-DuduwQoe.svg  6.87 kB │ gzip: 2.72 kB
2026-02-13T19:45:49.2435920Z dist-react/assets/man_in_motorized_wheelchair_tone5-CgvQDAuT.svg  6.87 kB │ gzip: 2.72 kB
2026-02-13T19:45:49.2436132Z dist-react/assets/flag_ky-E8sT-Yzf.svg  6.99 kB │ gzip: 2.92 kB
2026-02-13T19:45:49.2436186Z dist-react/assets/anatomical_heart-DbQDqK_8.svg  7.00 kB │ gzip: 3.14 kB
2026-02-13T19:45:49.2436238Z dist-react/assets/wales-ll0ySOk-.svg  7.01 kB │ gzip: 2.89 kB
2026-02-13T19:45:49.2436297Z dist-react/assets/woman_in_motorized_wheelchair-CIaEP3y5.svg  7.03 kB │ gzip: 2.79 kB
2026-02-13T19:45:49.2436354Z dist-react/assets/woman_in_motorized_wheelchair_tone2-uhLYilhF.svg  7.03 kB │ gzip: 2.79 kB
2026-02-13T19:45:49.2436407Z dist-react/assets/woman_in_motorized_wheelchair_tone1-1BibIgKr.svg  7.03 kB │ gzip: 2.78 kB
2026-02-13T19:45:49.2436462Z dist-react/assets/woman_in_motorized_wheelchair_tone3-B51r71l0.svg  7.03 kB │ gzip: 2.79 kB
2026-02-13T19:45:49.2436515Z dist-react/assets/woman_in_motorized_wheelchair_tone4-oIvpxZcp.svg  7.03 kB │ gzip: 2.79 kB
2026-02-13T19:45:49.2436572Z dist-react/assets/woman_in_motorized_wheelchair_tone5-_fFN26h0.svg  7.03 kB │ gzip: 2.79 kB
2026-02-13T19:45:49.2436626Z dist-react/assets/woman_bouncing_ball-B4V8jGG-.svg  7.09 kB │ gzip: 3.20 kB
2026-02-13T19:45:49.2436680Z dist-react/assets/woman_bouncing_ball_tone2-BPyPsinZ.svg  7.09 kB │ gzip: 3.21 kB
2026-02-13T19:45:49.2436734Z dist-react/assets/woman_bouncing_ball_tone1-I7gUQpbX.svg  7.09 kB │ gzip: 3.21 kB
2026-02-13T19:45:49.2436790Z dist-react/assets/woman_bouncing_ball_tone3-UqVs8gxM.svg  7.09 kB │ gzip: 3.21 kB
2026-02-13T19:45:49.2436846Z dist-react/assets/woman_bouncing_ball_tone4-CtQI59zT.svg  7.09 kB │ gzip: 3.21 kB
2026-02-13T19:45:49.2436899Z dist-react/assets/woman_bouncing_ball_tone5-BgHu12i2.svg  7.09 kB │ gzip: 3.21 kB
2026-02-13T19:45:49.2436950Z dist-react/assets/flag_va-BB2uDrB0.svg  7.21 kB │ gzip: 2.49 kB
2026-02-13T19:45:49.2437003Z dist-react/assets/mammoth-Diaisynz.svg  7.29 kB │ gzip: 3.06 kB
2026-02-13T19:45:49.2437061Z dist-react/assets/nest_with_eggs-C5ulh3Rz.svg  7.36 kB │ gzip: 3.39 kB
2026-02-13T19:45:49.2437115Z dist-react/assets/phoenix-QKXqSCuH.svg  7.57 kB │ gzip: 2.91 kB
2026-02-13T19:45:49.2437167Z dist-react/assets/flag_cy-JKjUtxO9.svg  7.60 kB │ gzip: 3.32 kB
2026-02-13T19:45:49.2437220Z dist-react/assets/people_with_bunny_ears_partying-BVR6SBwD.svg  7.63 kB │ gzip: 1.93 kB
2026-02-13T19:45:49.2437276Z dist-react/assets/flag_gu-CyZZwWUz.svg  7.68 kB │ gzip: 3.24 kB
2026-02-13T19:45:49.2437338Z dist-react/assets/mute-CFH4QcSE.mp3  7.74 kB
2026-02-13T19:45:49.2437391Z dist-react/assets/t_rex-BYG-fgI4.svg  8.00 kB │ gzip: 3.35 kB
2026-02-13T19:45:49.2437442Z dist-react/assets/flag_vg-DWuAWiyw.svg  8.21 kB │ gzip: 1.91 kB
2026-02-13T19:45:49.2437497Z dist-react/assets/flag_yt-BfOxXbO5.svg  8.42 kB │ gzip: 2.95 kB
2026-02-13T19:45:49.2437548Z dist-react/assets/piñata-CQK6iMPe.svg  8.47 kB │ gzip: 3.21 kB
2026-02-13T19:45:49.2437605Z dist-react/assets/mirror_ball-R_criUm_.svg  8.55 kB │ gzip: 3.31 kB
2026-02-13T19:45:49.2437659Z dist-react/assets/ping-LfakLpwb.mp3  8.58 kB
2026-02-13T19:45:49.2437733Z dist-react/assets/flag_gs-DhFNtBGF.svg  8.86 kB │ gzip: 3.67 kB
2026-02-13T19:45:49.2437792Z dist-react/assets/knot-CpRGiIMe.svg  8.92 kB │ gzip: 3.89 kB
2026-02-13T19:45:49.2437847Z dist-react/assets/flag_dg-DwJEN7pv.svg  9.06 kB │ gzip: 2.87 kB
2026-02-13T19:45:49.2437902Z dist-react/assets/flag_gt-CietPgvg.svg  9.11 kB │ gzip: 3.86 kB
2026-02-13T19:45:49.2437956Z dist-react/assets/flag_mx-g-aNhK9D.svg  9.66 kB │ gzip: 3.72 kB
2026-02-13T19:45:49.2438009Z dist-react/assets/flag_ta-Q6DTxsoW.svg  10.30 kB │ gzip: 4.05 kB
2026-02-13T19:45:49.2438062Z dist-react/assets/flag_je-CGBxZBdT.svg  10.35 kB │ gzip: 4.17 kB
2026-02-13T19:45:49.2438119Z dist-react/assets/unmute-BDrGNM7s.mp3  10.67 kB
2026-02-13T19:45:49.2438170Z dist-react/assets/flag_do-sBcfT32z.svg  11.37 kB │ gzip: 4.67 kB
2026-02-13T19:45:49.2438223Z dist-react/assets/deafen-BWE6ozKl.mp3  11.92 kB
2026-02-13T19:45:49.2438275Z dist-react/assets/flag_sa-B3EC8eCD.svg  12.29 kB │ gzip: 5.12 kB
2026-02-13T19:45:49.2438337Z dist-react/assets/flag_al-D439po3l.svg  12.43 kB │ gzip: 5.32 kB
2026-02-13T19:45:49.2438392Z dist-react/assets/undeafen-DHeUZCMd.mp3  13.59 kB
2026-02-13T19:45:49.2438449Z dist-react/assets/flag_bz-B34xZjVJ.svg  13.64 kB │ gzip: 5.44 kB
2026-02-13T19:45:49.2438503Z dist-react/assets/flag_pm-C-C2d-w4.svg  13.65 kB │ gzip: 3.80 kB
2026-02-13T19:45:49.2438562Z dist-react/assets/leave_call-DtItyZMg.mp3  14.43 kB
2026-02-13T19:45:49.2438616Z dist-react/assets/flag_nf-BjOIhoMF.svg  14.69 kB │ gzip: 5.98 kB
2026-02-13T19:45:49.2438669Z dist-react/assets/screenshare_start-B8JctOq8.mp3  16.13 kB
2026-02-13T19:45:49.2438721Z dist-react/assets/flag_ac-Dr8n8VBW.svg  16.66 kB │ gzip: 5.62 kB
2026-02-13T19:45:49.2438774Z dist-react/assets/screenshare_stop-DhppajDk.mp3  18.43 kB
2026-02-13T19:45:49.2438829Z dist-react/assets/potted_plant-BHg6K0D8.svg  21.00 kB │ gzip: 8.96 kB
2026-02-13T19:45:49.2438900Z dist-react/assets/flag_mp-Bs0Xr_ND.svg  24.06 kB │ gzip: 9.57 kB
2026-02-13T19:45:49.2438955Z dist-react/assets/flag_af-CN78RMpg.svg  24.13 kB │ gzip: 9.18 kB
2026-02-13T19:45:49.2439007Z dist-react/assets/flag_kz-D77IkgDL.svg  26.58 kB │ gzip: 9.02 kB
2026-02-13T19:45:49.2439061Z dist-react/assets/united_nations-BC9awctQ.svg  26.58 kB │ gzip: 10.32 kB
2026-02-13T19:45:49.2439118Z dist-react/assets/join_call-B65a9Ev2.mp3  30.54 kB
2026-02-13T19:45:49.2439173Z dist-react/assets/gg sans Regular-Bd8GJPVd.woff  39.09 kB
2026-02-13T19:45:49.2439225Z dist-react/assets/emojies_greyscale-CtRIvx0g.png  39.11 kB
2026-02-13T19:45:49.2439276Z dist-react/assets/gg sans Bold-BGlwbW8t.woff  40.13 kB
2026-02-13T19:45:49.2439331Z dist-react/assets/gg sans Medium-BMWm4JFW.woff  40.32 kB
2026-02-13T19:45:49.2439384Z dist-react/assets/gg sans Semibold-xAGa8zYH.woff  40.57 kB
2026-02-13T19:45:49.2439435Z dist-react/assets/emojies_colored-Cxo2u_zo.png  45.89 kB
2026-02-13T19:45:49.2439487Z dist-react/assets/screenshare_viewer_join-BOPrADSV.mp3  67.54 kB
2026-02-13T19:45:49.2439542Z dist-react/assets/screenshare_viewer_leave-BoDMhfvJ.mp3  67.54 kB
2026-02-13T19:45:49.2439594Z dist-react/assets/index-DN2VsIhS.css  72.50 kB │ gzip: 12.96 kB
2026-02-13T19:45:49.2439653Z dist-react/assets/index-9riexBjR.js 8,425.21 kB │ gzip: 1,537.92 kB
2026-02-13T19:45:49.2439708Z 
2026-02-13T19:45:49.2439745Z (!) Some chunks are larger than 1000 kB after minification. Consider:
2026-02-13T19:45:49.2439792Z - Using dynamic import() to code-split the application
2026-02-13T19:45:49.2439860Z - Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
2026-02-13T19:45:49.2439925Z - Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
2026-02-13T19:45:49.2440027Z ✓ built in 3.79s
2026-02-13T19:45:49.6728276Z • electron-builder version=25.1.8 os=6.12.54-Unraid
2026-02-13T19:45:49.6730876Z • artifacts will be published if draft release exists reason=CI detected
2026-02-13T19:45:49.6747756Z • loaded configuration file=package.json ("build" field)
2026-02-13T19:45:49.7516508Z • installing production dependencies platform=linux arch=x64 appDir=/workspace/Moyettes/DiscordClone/apps/electron
2026-02-13T19:45:50.3522962Z • executing @electron/rebuild electronVersion=33.4.11 arch=x64 buildFromSource=false appDir=./
2026-02-13T19:45:50.3527333Z • installing native dependencies arch=x64
2026-02-13T19:45:50.3691072Z  node:internal/modules/cjs/loader:1228
2026-02-13T19:45:50.3691374Z throw err;
2026-02-13T19:45:50.3691456Z ^
2026-02-13T19:45:50.3691567Z
2026-02-13T19:45:50.3691656Z Error: Cannot find module '/workspace/Moyettes/DiscordClone/node_modules/app-builder-lib/out/util/rebuild/remote-rebuild.js'
2026-02-13T19:45:50.3691762Z  at Function._resolveFilename (node:internal/modules/cjs/loader:1225:15)
2026-02-13T19:45:50.3691876Z  at Function._load (node:internal/modules/cjs/loader:1055:27)
2026-02-13T19:45:50.3691956Z  at TracingChannel.traceSync (node:diagnostics_channel:322:14)
2026-02-13T19:45:50.3692038Z  at wrapModuleLoad (node:internal/modules/cjs/loader:220:24)
2026-02-13T19:45:50.3692114Z  at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5)
2026-02-13T19:45:50.3692210Z  at node:internal/main/run_main_module:36:49 {
2026-02-13T19:45:50.3692298Z code: 'MODULE_NOT_FOUND',
2026-02-13T19:45:50.3692367Z requireStack: []
2026-02-13T19:45:50.3692434Z }
2026-02-13T19:45:50.3692493Z
2026-02-13T19:45:50.3692566Z Node.js v22.14.0
2026-02-13T19:45:50.3692636Z
2026-02-13T19:45:50.3716284Z  Rebuilder failed with exit code: 1 failedTask=build stackTrace=Error: Rebuilder failed with exit code: 1
2026-02-13T19:45:50.3716453Z at ChildProcess.<anonymous> (/workspace/Moyettes/DiscordClone/node_modules/app-builder-lib/out/util/rebuild/rebuild.js:54:40)
2026-02-13T19:45:50.3716554Z at ChildProcess.emit (node:events:518:28)
2026-02-13T19:45:50.3716632Z at Process.ChildProcess._handle.onexit (node:internal/child_process:293:12)
2026-02-13T19:45:50.3919709Z ❌ Failure - Main Build Electron app
2026-02-13T19:45:50.3947186Z exitcode '1': failure
2026-02-13T19:45:50.4343747Z expression 'npm-electron-${{ hashFiles('package-lock.json') }}' rewritten to 'format('npm-electron-{0}', hashFiles('package-lock.json'))'
2026-02-13T19:45:50.4344005Z evaluating expression 'format('npm-electron-{0}', hashFiles('package-lock.json'))'
2026-02-13T19:45:50.4344271Z Writing entry to tarball workflow/hashfiles/index.js len:168437
2026-02-13T19:45:50.4346221Z Extracting content to '/var/run/act'
2026-02-13T19:45:50.4357201Z 🐳 docker exec cmd=[node /var/run/act/workflow/hashfiles/index.js] user= workdir=
2026-02-13T19:45:50.4357547Z Exec command '[node /var/run/act/workflow/hashfiles/index.js]'
2026-02-13T19:45:50.4357806Z Working directory '/workspace/Moyettes/DiscordClone'
2026-02-13T19:45:50.5032272Z expression 'format('npm-electron-{0}', hashFiles('package-lock.json'))' evaluated to '%!t(string=npm-electron-3bfeb88fe20d0eab8d1e9deb0c16af4cc320445b15ebe7f6fa7d2e30a7316852)'
2026-02-13T19:45:50.5074002Z evaluating expression 'success()'
2026-02-13T19:45:50.5074389Z expression 'success()' evaluated to 'false'
2026-02-13T19:45:50.5074538Z Skipping step 'Cache npm and Electron' due to 'success()'
2026-02-13T19:45:50.5182082Z evaluating expression 'always()'
2026-02-13T19:45:50.5182485Z expression 'always()' evaluated to 'true'
2026-02-13T19:45:50.5182588Z ⭐ Run Post Checkout repository
2026-02-13T19:45:50.5182776Z Writing entry to tarball workflow/outputcmd.txt len:0
2026-02-13T19:45:50.5182920Z Writing entry to tarball workflow/statecmd.txt len:0
2026-02-13T19:45:50.5183011Z Writing entry to tarball workflow/pathcmd.txt len:0
2026-02-13T19:45:50.5183097Z Writing entry to tarball workflow/envs.txt len:0
2026-02-13T19:45:50.5183183Z Writing entry to tarball workflow/SUMMARY.md len:0
2026-02-13T19:45:50.5183271Z Extracting content to '/var/run/act'
2026-02-13T19:45:50.5193581Z run post step for 'Checkout repository'
2026-02-13T19:45:50.5194070Z executing remote job container: [node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]
2026-02-13T19:45:50.5194221Z 🐳 docker exec cmd=[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js] user= workdir=
2026-02-13T19:45:50.5194328Z Exec command '[node /var/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/index.js]'
2026-02-13T19:45:50.5194571Z Working directory '/workspace/Moyettes/DiscordClone'
2026-02-13T19:45:50.5980034Z [command]/usr/bin/git version
2026-02-13T19:45:50.6004447Z git version 2.34.1
2026-02-13T19:45:50.6023930Z ***
2026-02-13T19:45:50.6030965Z Copying '/root/.gitconfig' to '/tmp/60a666f6-d59c-408a-a1cb-056ca0b4c502/.gitconfig'
2026-02-13T19:45:50.6037156Z Temporarily overriding HOME='/tmp/60a666f6-d59c-408a-a1cb-056ca0b4c502' before making global git config changes
2026-02-13T19:45:50.6038350Z Adding repository directory to the temporary git global config as a safe directory
2026-02-13T19:45:50.6042170Z [command]/usr/bin/git config --global --add safe.directory /workspace/Moyettes/DiscordClone
2026-02-13T19:45:50.6065501Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
2026-02-13T19:45:50.6093861Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
2026-02-13T19:45:50.6267924Z [command]/usr/bin/git config --local --name-only --get-regexp http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader
2026-02-13T19:45:50.6283475Z http.http://192.168.125.15:4000/.extraheader
2026-02-13T19:45:50.6291176Z [command]/usr/bin/git config --local --unset-all http.http://192.168.125.15:4000/.extraheader
2026-02-13T19:45:50.6310983Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.http\:\/\/192\.168\.125\.15\:4000\/\.extraheader' && git config --local --unset-all 'http.http://192.168.125.15:4000/.extraheader' || :"
2026-02-13T19:45:50.6473927Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
2026-02-13T19:45:50.6494369Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
2026-02-13T19:45:50.6719307Z ✅ Success - Post Checkout repository
2026-02-13T19:45:50.6746343Z Cleaning up container for job build-and-release
2026-02-13T19:46:19.4915273Z Removed container: 78088ba67b9e9a8de8380d20c89ea44da62bb5887fd0db970d18137ccf97f44f
2026-02-13T19:46:19.4936249Z 🐳 docker volume rm GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release
2026-02-13T19:46:19.9625914Z 🐳 docker volume rm GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release-env
2026-02-13T19:46:20.2859684Z Cleaning up network for job build-and-release, and network name is: GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release-build-and-release-network
2026-02-13T19:46:21.1942946Z 🏁 Job failed
2026-02-13T19:46:21.1982622Z Job 'build-and-release' failed

View File

@@ -174,9 +174,27 @@ async function decryptBatch(items) {
})); }));
} }
// --- Ed25519 Support Detection ---
let ed25519Supported = null;
async function checkEd25519Support() {
if (ed25519Supported !== null) return ed25519Supported;
try {
await crypto.subtle.generateKey('Ed25519', false, ['sign', 'verify']);
ed25519Supported = true;
} catch {
ed25519Supported = false;
}
return ed25519Supported;
}
// --- Batch Verify --- // --- Batch Verify ---
async function verifyBatch(items) { async function verifyBatch(items) {
const supported = await checkEd25519Support();
if (!supported) {
return items.map(() => ({ success: true, verified: null }));
}
return Promise.all(items.map(async ({ publicKey, message, signature }) => { return Promise.all(items.map(async ({ publicKey, message, signature }) => {
try { try {
const verified = await verifySignature(publicKey, message, signature); const verified = await verifySignature(publicKey, message, signature);

View File

@@ -1,7 +1,7 @@
{ {
"name": "@discord-clone/shared", "name": "@discord-clone/shared",
"private": true, "private": true,
"version": "1.0.14", "version": "1.0.16",
"type": "module", "type": "module",
"main": "src/App.jsx", "main": "src/App.jsx",
"dependencies": { "dependencies": {

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect } from 'react';
import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom'; import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
import Login from './pages/Login'; import Login from './pages/Login';
import Register from './pages/Register'; import Register from './pages/Register';
@@ -62,21 +62,18 @@ function AuthGuard({ children }) {
return () => { cancelled = true; }; return () => { cancelled = true; };
}, []); }, []);
// Redirect once after auth state is determined (not on every route change)
const hasRedirected = useRef(false);
useEffect(() => { useEffect(() => {
if (authState === 'loading' || hasRedirected.current) return; if (authState === 'loading') return;
hasRedirected.current = true;
const isAuthPage = location.pathname === '/' || location.pathname === '/register'; const isAuthPage = location.pathname === '/' || location.pathname === '/register';
const hasSession = sessionStorage.getItem('privateKey') && sessionStorage.getItem('signingKey');
if (authState === 'authenticated' && isAuthPage) { if (hasSession && isAuthPage) {
navigate('/chat', { replace: true }); navigate('/chat', { replace: true });
} else if (authState === 'unauthenticated' && !isAuthPage) { } else if (!hasSession && !isAuthPage) {
navigate('/', { replace: true }); navigate('/', { replace: true });
} }
}, [authState]); }, [authState, location.pathname]);
if (authState === 'loading') { if (authState === 'loading') {
return ( return (

View File

@@ -24,7 +24,9 @@ import UserProfilePopup from './UserProfilePopup';
import Avatar from './Avatar'; import Avatar from './Avatar';
import MentionMenu from './MentionMenu'; import MentionMenu from './MentionMenu';
import MessageItem, { getUserColor } from './MessageItem'; import MessageItem, { getUserColor } from './MessageItem';
import ColoredIcon from './ColoredIcon';
import { usePlatform } from '../platform'; import { usePlatform } from '../platform';
import { useVoice } from '../contexts/VoiceContext';
const metadataCache = new Map(); const metadataCache = new Map();
const attachmentCache = new Map(); const attachmentCache = new Map();
@@ -59,8 +61,8 @@ export function clearMessageDecryptionCache() {
messageDecryptionCache.clear(); messageDecryptionCache.clear();
} }
const ICON_COLOR_DEFAULT = 'color-mix(in oklab, hsl(240 4.294% 68.039% / 1) 100%, #000 0%)'; const ICON_COLOR_DEFAULT = 'hsl(240, 4.294%, 68.039%)';
const ICON_COLOR_DANGER = 'color-mix(in oklab, hsl(1.353 calc(1*82.609%) 68.431% /1) 100%, #000 0%)'; const ICON_COLOR_DANGER = 'hsl(1.353, 82.609%, 68.431%)';
const fromHexString = (hexString) => const fromHexString = (hexString) =>
new Uint8Array(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))); new Uint8Array(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
@@ -463,14 +465,33 @@ const MessageContextMenu = ({ x, y, onInteract, onClose, isOwner, canDelete }) =
); );
}; };
const ColoredIcon = ({ src, color, size = '24px', style = {} }) => ( const InputContextMenu = ({ x, y, onClose, onPaste }) => {
<div style={{ width: size, height: size, overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', ...style }}> const menuRef = useRef(null);
<img src={src} alt="" style={color ? { width: size, height: size, transform: 'translateX(-1000px)', filter: `drop-shadow(1000px 0 0 ${color})` } : { width: size, height: size, objectFit: 'contain' }} /> const [pos, setPos] = useState({ top: y, left: x });
useEffect(() => { const h = () => onClose(); window.addEventListener('click', h); return () => window.removeEventListener('click', h); }, [onClose]);
React.useLayoutEffect(() => {
if (!menuRef.current) return;
const rect = menuRef.current.getBoundingClientRect();
let newTop = y, newLeft = x;
if (x + rect.width > window.innerWidth) newLeft = x - rect.width;
if (y + rect.height > window.innerHeight) newTop = y - rect.height;
if (newLeft < 0) newLeft = 10;
if (newTop < 0) newTop = 10;
setPos({ top: newTop, left: newLeft });
}, [x, y]);
return (
<div ref={menuRef} className="context-menu" style={{ top: pos.top, left: pos.left }} onClick={(e) => e.stopPropagation()}>
<div className="context-menu-item" onClick={(e) => { e.stopPropagation(); onPaste(); onClose(); }}>
<span>Paste</span>
</div> </div>
); </div>
);
};
const ChatArea = ({ channelId, channelName, channelType, username, channelKey, userId: currentUserId, showMembers, onToggleMembers, onOpenDM, showPinned, onTogglePinned }) => { const ChatArea = ({ channelId, channelName, channelType, username, channelKey, userId: currentUserId, showMembers, onToggleMembers, onOpenDM, showPinned, onTogglePinned }) => {
const { crypto } = usePlatform(); const { crypto } = usePlatform();
const { isReceivingScreenShareAudio } = useVoice();
const [decryptedMessages, setDecryptedMessages] = useState([]); const [decryptedMessages, setDecryptedMessages] = useState([]);
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [zoomedImage, setZoomedImage] = useState(null); const [zoomedImage, setZoomedImage] = useState(null);
@@ -481,6 +502,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
const [isMultiline, setIsMultiline] = useState(false); const [isMultiline, setIsMultiline] = useState(false);
const [hoveredMessageId, setHoveredMessageId] = useState(null); const [hoveredMessageId, setHoveredMessageId] = useState(null);
const [contextMenu, setContextMenu] = useState(null); const [contextMenu, setContextMenu] = useState(null);
const [inputContextMenu, setInputContextMenu] = useState(null);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [replyingTo, setReplyingTo] = useState(null); const [replyingTo, setReplyingTo] = useState(null);
const [editingMessage, setEditingMessage] = useState(null); const [editingMessage, setEditingMessage] = useState(null);
@@ -563,7 +585,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
return { return {
...msg, ...msg,
content: cached?.content ?? '[Decrypting...]', content: cached?.content ?? '[Decrypting...]',
isVerified: cached?.isVerified ?? false, isVerified: cached?.isVerified ?? null,
decryptedReply: cached?.decryptedReply ?? null, decryptedReply: cached?.decryptedReply ?? null,
}; };
}); });
@@ -667,14 +689,15 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
const verifyMap = new Map(); const verifyMap = new Map();
for (let i = 0; i < verifyResults.length; i++) { for (let i = 0; i < verifyResults.length; i++) {
const msg = verifyMsgMap[i]; const msg = verifyMsgMap[i];
verifyMap.set(msg.id, verifyResults[i].success && verifyResults[i].verified); const verified = verifyResults[i].verified;
verifyMap.set(msg.id, verified === null ? null : (verifyResults[i].success && verified));
} }
// Populate cache // Populate cache
for (const msg of needsDecryption) { for (const msg of needsDecryption) {
const content = decryptedMap.get(msg.id) ?? const content = decryptedMap.get(msg.id) ??
(msg.ciphertext && msg.ciphertext.length < TAG_LENGTH ? '[Invalid Encrypted Message]' : '[Encrypted Message - Key Missing]'); (msg.ciphertext && msg.ciphertext.length < TAG_LENGTH ? '[Invalid Encrypted Message]' : '[Encrypted Message - Key Missing]');
const isVerified = verifyMap.get(msg.id) ?? false; const isVerified = verifyMap.has(msg.id) ? verifyMap.get(msg.id) : null;
const decryptedReply = replyMap.get(msg.id) ?? null; const decryptedReply = replyMap.get(msg.id) ?? null;
messageDecryptionCache.set(msg.id, { content, isVerified, decryptedReply }); messageDecryptionCache.set(msg.id, { content, isVerified, decryptedReply });
} }
@@ -715,13 +738,14 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
}, [username, myRoleNames]); }, [username, myRoleNames]);
const playPingSound = useCallback(() => { const playPingSound = useCallback(() => {
if (isReceivingScreenShareAudio) return;
const now = Date.now(); const now = Date.now();
if (now - lastPingTimeRef.current < 1000) return; if (now - lastPingTimeRef.current < 1000) return;
lastPingTimeRef.current = now; lastPingTimeRef.current = now;
const audio = new Audio(PingSound); const audio = new Audio(PingSound);
audio.volume = 0.5; audio.volume = 0.5;
audio.play().catch(() => {}); audio.play().catch(() => {});
}, []); }, [isReceivingScreenShareAudio]);
// Play ping sound when a new message mentions us (by username or role) // Play ping sound when a new message mentions us (by username or role)
useEffect(() => { useEffect(() => {
@@ -1341,6 +1365,38 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
</div> </div>
</div> </div>
{contextMenu && <MessageContextMenu x={contextMenu.x} y={contextMenu.y} isOwner={contextMenu.isOwner} canDelete={contextMenu.canDelete} onClose={() => setContextMenu(null)} onInteract={(action) => handleContextInteract(action, contextMenu.messageId)} />} {contextMenu && <MessageContextMenu x={contextMenu.x} y={contextMenu.y} isOwner={contextMenu.isOwner} canDelete={contextMenu.canDelete} onClose={() => setContextMenu(null)} onInteract={(action) => handleContextInteract(action, contextMenu.messageId)} />}
{inputContextMenu && <InputContextMenu x={inputContextMenu.x} y={inputContextMenu.y} onClose={() => setInputContextMenu(null)} onPaste={async () => {
try {
if (inputDivRef.current) inputDivRef.current.focus();
// Try reading clipboard items for images first
if (navigator.clipboard.read) {
try {
const items = await navigator.clipboard.read();
for (const item of items) {
const imageType = item.types.find(t => t.startsWith('image/'));
if (imageType) {
const blob = await item.getType(imageType);
const file = new File([blob], `pasted-image.${imageType.split('/')[1] || 'png'}`, { type: imageType });
processFile(file);
return;
}
}
} catch {}
}
// Fall back to plain text
const text = await navigator.clipboard.readText();
if (text) {
document.execCommand('insertText', false, text);
// Sync state — onInput may not fire from async execCommand
const el = inputDivRef.current;
if (el) {
setInput(el.textContent);
const inner = el.innerText;
setIsMultiline(inner.includes('\n') || el.scrollHeight > 50);
}
}
} catch {}
}} />}
<form className="chat-input-form" onSubmit={handleSend} style={{ position: 'relative' }}> <form className="chat-input-form" onSubmit={handleSend} style={{ position: 'relative' }}>
{mentionQuery !== null && mentionItems.length > 0 && ( {mentionQuery !== null && mentionItems.length > 0 && (
@@ -1382,6 +1438,10 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u
onBlur={saveSelection} onBlur={saveSelection}
onMouseUp={saveSelection} onMouseUp={saveSelection}
onKeyUp={saveSelection} onKeyUp={saveSelection}
onContextMenu={(e) => {
e.preventDefault();
setInputContextMenu({ x: e.clientX, y: e.clientY });
}}
onPaste={(e) => { onPaste={(e) => {
const items = e.clipboardData?.items; const items = e.clipboardData?.items;
if (items) { if (items) {

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import Tooltip from './Tooltip'; import Tooltip from './Tooltip';
const ChatHeader = ({ channelName, channelType, channelTopic, onToggleMembers, showMembers, onTogglePinned, serverName }) => { const ChatHeader = ({ channelName, channelType, channelTopic, onToggleMembers, showMembers, onTogglePinned, serverName, isMobile, onMobileBack }) => {
const [searchFocused, setSearchFocused] = useState(false); const [searchFocused, setSearchFocused] = useState(false);
const isDM = channelType === 'dm'; const isDM = channelType === 'dm';
@@ -10,9 +10,14 @@ const ChatHeader = ({ channelName, channelType, channelTopic, onToggleMembers, s
return ( return (
<div className="chat-header"> <div className="chat-header">
<div className="chat-header-left"> <div className="chat-header-left">
{isMobile && onMobileBack && (
<button className="mobile-back-btn" onClick={onMobileBack}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
)}
<span className="chat-header-icon">{isDM ? '@' : '#'}</span> <span className="chat-header-icon">{isDM ? '@' : '#'}</span>
<span className="chat-header-name">{channelName}</span> <span className="chat-header-name">{channelName}</span>
{channelTopic && !isDM && ( {channelTopic && !isDM && !isMobile && (
<> <>
<div className="chat-header-divider" /> <div className="chat-header-divider" />
<span className="chat-header-topic" title={channelTopic}>{channelTopic}</span> <span className="chat-header-topic" title={channelTopic}>{channelTopic}</span>
@@ -21,7 +26,7 @@ const ChatHeader = ({ channelName, channelType, channelTopic, onToggleMembers, s
{isDM && <span className="chat-header-status-text"></span>} {isDM && <span className="chat-header-status-text"></span>}
</div> </div>
<div className="chat-header-right"> <div className="chat-header-right">
{!isDM && ( {!isDM && !isMobile && (
<Tooltip text="Threads" position="bottom"> <Tooltip text="Threads" position="bottom">
<button className="chat-header-btn"> <button className="chat-header-btn">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
@@ -37,7 +42,7 @@ const ChatHeader = ({ channelName, channelType, channelTopic, onToggleMembers, s
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>
{!isDM && ( {!isDM && !isMobile && (
<Tooltip text={showMembers ? "Hide Members" : "Show Members"} position="bottom"> <Tooltip text={showMembers ? "Hide Members" : "Show Members"} position="bottom">
<button <button
className={`chat-header-btn ${showMembers ? 'active' : ''}`} className={`chat-header-btn ${showMembers ? 'active' : ''}`}
@@ -50,6 +55,7 @@ const ChatHeader = ({ channelName, channelType, channelTopic, onToggleMembers, s
</button> </button>
</Tooltip> </Tooltip>
)} )}
{!isMobile && (
<Tooltip text="Notification Settings" position="bottom"> <Tooltip text="Notification Settings" position="bottom">
<button className="chat-header-btn"> <button className="chat-header-btn">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"> <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
@@ -57,6 +63,8 @@ const ChatHeader = ({ channelName, channelType, channelTopic, onToggleMembers, s
</svg> </svg>
</button> </button>
</Tooltip> </Tooltip>
)}
{!isMobile && (
<div className="chat-header-search-wrapper"> <div className="chat-header-search-wrapper">
<input <input
type="text" type="text"
@@ -66,6 +74,7 @@ const ChatHeader = ({ channelName, channelType, channelTopic, onToggleMembers, s
onBlur={() => setSearchFocused(false)} onBlur={() => setSearchFocused(false)}
/> />
</div> </div>
)}
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,31 @@
import React from 'react';
const ColoredIcon = React.memo(({ src, color, size = '20px', style = {} }) => {
if (!color) {
return (
<div style={{ width: size, height: size, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, ...style }}>
<img src={src} alt="" style={{ width: size, height: size, objectFit: 'contain' }} />
</div>
);
}
return (
<div style={{
width: size, height: size, flexShrink: 0,
overflow: 'hidden',
...style,
}}>
<img
src={src}
alt=""
style={{
width: size, height: size,
objectFit: 'contain',
filter: `drop-shadow(${size} 0 0 ${color})`,
transform: `translateX(-${size})`,
}}
/>
</div>
);
});
export default ColoredIcon;

View File

@@ -3,6 +3,7 @@ import { useConvex } from 'convex/react';
import { api } from '../../../../convex/_generated/api'; import { api } from '../../../../convex/_generated/api';
import Tooltip from './Tooltip'; import Tooltip from './Tooltip';
import Avatar from './Avatar'; import Avatar from './Avatar';
import ColoredIcon from './ColoredIcon';
import { useOnlineUsers } from '../contexts/PresenceContext'; import { useOnlineUsers } from '../contexts/PresenceContext';
import friendsIcon from '../assets/icons/friends.svg'; import friendsIcon from '../assets/icons/friends.svg';
@@ -181,26 +182,7 @@ const DMList = ({ dmChannels, activeDMChannel, onSelectDM, onOpenDM }) => {
onClick={() => onSelectDM('friends')} onClick={() => onSelectDM('friends')}
> >
<div style={{ marginRight: '12px' }}> <div style={{ marginRight: '12px' }}>
<div style={{ <ColoredIcon src={friendsIcon} color="var(--interactive-normal)" size="24px" />
width: 24,
height: 24,
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
}}>
<img
src={friendsIcon}
alt=""
style={{
width: 24,
height: 24,
transform: 'translateX(-1000px)',
filter: `drop-shadow(1000px 0 0 var(--interactive-normal))`
}}
/>
</div>
</div> </div>
<span style={{ fontWeight: 500 }}>Friends</span> <span style={{ fontWeight: 500 }}>Friends</span>
</div> </div>

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { useQuery } from 'convex/react'; import { useQuery } from 'convex/react';
import { api } from '../../../../convex/_generated/api'; import { api } from '../../../../convex/_generated/api';
import Avatar from './Avatar'; import Avatar from './Avatar';
import ColoredIcon from './ColoredIcon';
import { useOnlineUsers } from '../contexts/PresenceContext'; import { useOnlineUsers } from '../contexts/PresenceContext';
import friendsIcon from '../assets/icons/friends.svg'; import friendsIcon from '../assets/icons/friends.svg';
@@ -54,26 +55,7 @@ const FriendsView = ({ onOpenDM }) => {
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', marginRight: '16px', paddingRight: '16px', borderRight: '1px solid var(--border-subtle)' }}> <div style={{ display: 'flex', alignItems: 'center', marginRight: '16px', paddingRight: '16px', borderRight: '1px solid var(--border-subtle)' }}>
<div style={{ marginRight: '12px' }}> <div style={{ marginRight: '12px' }}>
<div style={{ <ColoredIcon src={friendsIcon} color="var(--interactive-normal)" size="24px" />
width: 24,
height: 24,
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
}}>
<img
src={friendsIcon}
alt=""
style={{
width: 24,
height: 24,
transform: 'translateX(-1000px)',
filter: `drop-shadow(1000px 0 0 var(--interactive-normal))`
}}
/>
</div>
</div> </div>
Friends Friends
</div> </div>

View File

@@ -4,6 +4,7 @@ import { api } from '../../../../convex/_generated/api';
import { useOnlineUsers } from '../contexts/PresenceContext'; import { useOnlineUsers } from '../contexts/PresenceContext';
import { useVoice } from '../contexts/VoiceContext'; import { useVoice } from '../contexts/VoiceContext';
import { CrownIcon, SharingIcon } from '../assets/icons'; import { CrownIcon, SharingIcon } from '../assets/icons';
import ColoredIcon from './ColoredIcon';
const USER_COLORS = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245']; const USER_COLORS = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245'];
@@ -15,12 +16,6 @@ function getUserColor(name) {
return USER_COLORS[Math.abs(hash) % USER_COLORS.length]; return USER_COLORS[Math.abs(hash) % USER_COLORS.length];
} }
const ColoredIcon = ({ src, color, size = '24px', style = {} }) => (
<div style={{ width: size, height: size, overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', ...style }}>
<img src={src} alt="" style={color ? { width: size, height: size, transform: 'translateX(-1000px)', filter: `drop-shadow(1000px 0 0 ${color})` } : { width: size, height: size, objectFit: 'contain' }} />
</div>
);
const STATUS_COLORS = { const STATUS_COLORS = {
online: '#3ba55c', online: '#3ba55c',
idle: '#faa61a', idle: '#faa61a',

View File

@@ -14,13 +14,14 @@ import {
import { getEmojiUrl, AllEmojis } from '../assets/emojis'; import { getEmojiUrl, AllEmojis } from '../assets/emojis';
import Tooltip from './Tooltip'; import Tooltip from './Tooltip';
import Avatar from './Avatar'; import Avatar from './Avatar';
import ColoredIcon from './ColoredIcon';
import { usePlatform } from '../platform'; import { usePlatform } from '../platform';
const fireIcon = getEmojiUrl('nature', 'fire'); const fireIcon = getEmojiUrl('nature', 'fire');
const heartIcon = getEmojiUrl('symbols', 'heart'); const heartIcon = getEmojiUrl('symbols', 'heart');
const thumbsupIcon = getEmojiUrl('people', 'thumbsup'); const thumbsupIcon = getEmojiUrl('people', 'thumbsup');
const ICON_COLOR_DEFAULT = 'color-mix(in oklab, hsl(240 4.294% 68.039% / 1) 100%, #000 0%)'; const ICON_COLOR_DEFAULT = 'hsl(240, 4.294%, 68.039%)';
const USER_COLORS = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245']; const USER_COLORS = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245'];
export const getUserColor = (name) => { export const getUserColor = (name) => {
@@ -99,12 +100,6 @@ const getReactionIcon = (name) => {
} }
}; };
const ColoredIcon = ({ src, color, size = '24px', style = {} }) => (
<div style={{ width: size, height: size, overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center', ...style }}>
<img src={src} alt="" style={color ? { width: size, height: size, transform: 'translateX(-1000px)', filter: `drop-shadow(1000px 0 0 ${color})` } : { width: size, height: size, objectFit: 'contain' }} />
</div>
);
const isNewDay = (current, previous) => { const isNewDay = (current, previous) => {
if (!previous) return true; if (!previous) return true;
return current.getDate() !== previous.getDate() return current.getDate() !== previous.getDate()
@@ -123,7 +118,7 @@ const createMarkdownComponents = (openExternal) => ({
return <span>{props.children}</span>; return <span>{props.children}</span>;
} }
} }
if (props.href && props.href.startsWith('mention://')) return <span style={{ background: 'color-mix(in oklab,hsl(234.935 calc(1*85.556%) 64.706% /0.23921568627450981) 100%,hsl(0 0% 0% /0.23921568627450981) 0%)', borderRadius: '3px', color: 'color-mix(in oklab, hsl(228.14 calc(1*100%) 83.137% /1) 100%, #000 0%)', fontWeight: 500, padding: '0 2px', cursor: 'default' }} title={props.children}>{props.children}</span>; if (props.href && props.href.startsWith('mention://')) return <span style={{ background: 'hsla(234.935, 85.556%, 64.706%, 0.239)', borderRadius: '3px', color: 'hsl(228.14, 100%, 83.137%)', fontWeight: 500, padding: '0 2px', cursor: 'default' }} title={props.children}>{props.children}</span>;
return <a {...props} onClick={(e) => { e.preventDefault(); openExternal(props.href); }} style={{ color: '#00b0f4', cursor: 'pointer', textDecoration: 'none' }} onMouseOver={(e) => e.target.style.textDecoration = 'underline'} onMouseOut={(e) => e.target.style.textDecoration = 'none'} />; return <a {...props} onClick={(e) => { e.preventDefault(); openExternal(props.href); }} style={{ color: '#00b0f4', cursor: 'pointer', textDecoration: 'none' }} onMouseOver={(e) => e.target.style.textDecoration = 'underline'} onMouseOut={(e) => e.target.style.textDecoration = 'none'} />;
}, },
code({ node, inline, className, children, ...props }) { code({ node, inline, className, children, ...props }) {
@@ -163,7 +158,7 @@ const MessageToolbar = ({ onAddReaction, onEdit, onReply, onMore, isOwner }) =>
<Tooltip text="Fire" position="top"> <Tooltip text="Fire" position="top">
<IconButton onClick={() => onAddReaction('fire')} emoji={<ColoredIcon src={fireIcon} size="20px" />} /> <IconButton onClick={() => onAddReaction('fire')} emoji={<ColoredIcon src={fireIcon} size="20px" />} />
</Tooltip> </Tooltip>
<div style={{ width: '1px', height: '24px', margin: '2px 4px', backgroundColor: 'color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.12156862745098039) 100%,hsl(0 0% 0% /0.12156862745098039) 0%)' }}></div> <div style={{ width: '1px', height: '24px', margin: '2px 4px', backgroundColor: 'hsla(240, 4%, 60.784%, 0.122)' }}></div>
<Tooltip text="Add Reaction" position="top"> <Tooltip text="Add Reaction" position="top">
<IconButton onClick={() => onAddReaction(null)} emoji={<ColoredIcon src={EmojieIcon} color={ICON_COLOR_DEFAULT} size="20px" />} /> <IconButton onClick={() => onAddReaction(null)} emoji={<ColoredIcon src={EmojieIcon} color={ICON_COLOR_DEFAULT} size="20px" />} />
</Tooltip> </Tooltip>
@@ -262,7 +257,7 @@ const MessageItem = React.memo(({
return ( return (
<div style={{ display: 'flex', gap: '4px', marginTop: '4px', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '4px', marginTop: '4px', flexWrap: 'wrap' }}>
{Object.entries(msg.reactions).map(([emojiName, data]) => ( {Object.entries(msg.reactions).map(([emojiName, data]) => (
<div key={emojiName} onClick={() => onReactionClick(msg.id, emojiName, data.me)} style={{ display: 'flex', alignItems: 'center', backgroundColor: data.me ? 'rgba(88, 101, 242, 0.15)' : 'hsl(240 calc(1*4%) 60.784% / 0.0784313725490196)', border: data.me ? '1px solid var(--brand-experiment)' : '1px solid transparent', borderRadius: '8px', padding: '2px 6px', cursor: 'pointer', gap: '4px' }}> <div key={emojiName} onClick={() => onReactionClick(msg.id, emojiName, data.me)} style={{ display: 'flex', alignItems: 'center', backgroundColor: data.me ? 'rgba(88, 101, 242, 0.15)' : 'hsla(240, 4%, 60.784%, 0.078)', border: data.me ? '1px solid var(--brand-experiment)' : '1px solid transparent', borderRadius: '8px', padding: '2px 6px', cursor: 'pointer', gap: '4px' }}>
<ColoredIcon src={getReactionIcon(emojiName)} size="16px" color={null} /> <ColoredIcon src={getReactionIcon(emojiName)} size="16px" color={null} />
<span style={{ fontSize: '12px', color: data.me ? '#dee0fc' : 'var(--header-secondary)', fontWeight: 600 }}>{data.count}</span> <span style={{ fontSize: '12px', color: data.me ? '#dee0fc' : 'var(--header-secondary)', fontWeight: 600 }}>{data.count}</span>
</div> </div>
@@ -287,12 +282,12 @@ const MessageItem = React.memo(({
<div <div
id={`msg-${msg.id}`} id={`msg-${msg.id}`}
className={`message-item${isGrouped ? ' message-grouped' : ''}`} className={`message-item${isGrouped ? ' message-grouped' : ''}`}
style={isMentioned ? { background: 'color-mix(in oklab,hsl(36.894 calc(1*100%) 31.569% /0.0784313725490196) 100%,hsl(0 0% 0% /0.0784313725490196) 0%)', position: 'relative' } : { position: 'relative' }} style={isMentioned ? { background: 'hsla(36.894, 100%, 31.569%, 0.078)', position: 'relative' } : { position: 'relative' }}
onMouseEnter={onHover} onMouseEnter={onHover}
onMouseLeave={onLeave} onMouseLeave={onLeave}
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
> >
{isMentioned && <div style={{ background: 'color-mix(in oklab, hsl(34 calc(1*50.847%) 53.725% /1) 100%, #000 0%)', bottom: 0, display: 'block', left: 0, pointerEvents: 'none', position: 'absolute', top: 0, width: '2px' }} />} {isMentioned && <div style={{ background: 'hsl(34, 50.847%, 53.725%)', bottom: 0, display: 'block', left: 0, pointerEvents: 'none', position: 'absolute', top: 0, width: '2px' }} />}
{msg.replyToId && msg.replyToUsername && ( {msg.replyToId && msg.replyToUsername && (
<div className="message-reply-context" onClick={() => onScrollToMessage(msg.replyToId)}> <div className="message-reply-context" onClick={() => onScrollToMessage(msg.replyToId)}>
@@ -330,7 +325,7 @@ const MessageItem = React.memo(({
> >
{msg.username || 'Unknown'} {msg.username || 'Unknown'}
</span> </span>
{!msg.isVerified && <span className="verification-failed" title="Signature Verification Failed!"><svg aria-hidden="true" role="img" width="16" height="16" viewBox="0 0 24 24" fill="var(--danger)"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path></svg></span>} {msg.isVerified === false && <span className="verification-failed" title="Signature Verification Failed!"><svg aria-hidden="true" role="img" width="16" height="16" viewBox="0 0 24 24" fill="var(--danger)"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path></svg></span>}
<span className="timestamp">{currentDate.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}</span> <span className="timestamp">{currentDate.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}</span>
</div> </div>
)} )}

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect, useLayoutEffect } from 'react'; import React, { useState, useRef, useEffect, useLayoutEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useConvex, useMutation, useQuery } from 'convex/react'; import { useConvex, useMutation, useQuery } from 'convex/react';
import { api } from '../../../../convex/_generated/api'; import { api } from '../../../../convex/_generated/api';
@@ -32,12 +32,13 @@ import screenShareStartSound from '../assets/sounds/screenshare_start.mp3';
import screenShareStopSound from '../assets/sounds/screenshare_stop.mp3'; import screenShareStopSound from '../assets/sounds/screenshare_stop.mp3';
import { getUserPref, setUserPref } from '../utils/userPreferences'; import { getUserPref, setUserPref } from '../utils/userPreferences';
import { usePlatform } from '../platform'; import { usePlatform } from '../platform';
import ColoredIcon from './ColoredIcon';
const USER_COLORS = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245']; const USER_COLORS = ['#5865F2', '#EBA7CD', '#57F287', '#FEE75C', '#EB459E', '#ED4245'];
const ICON_COLOR_DEFAULT = 'color-mix(in oklab, hsl(240 4.294% 68.039% / 1) 100%, #000 0%)'; const ICON_COLOR_DEFAULT = 'hsl(240, 4.294%, 68.039%)';
const ICON_COLOR_ACTIVE = 'hsl(357.692 67.826% 54.902% / 1)'; const ICON_COLOR_ACTIVE = 'hsl(357.692, 67.826%, 54.902%)';
const SERVER_MUTE_RED = 'color-mix(in oklab, hsl(1.343 calc(1*84.81%) 69.02% /1) 100%, #000 0%)'; const SERVER_MUTE_RED = 'hsl(1.343, 84.81%, 69.02%)';
const controlButtonStyle = { const controlButtonStyle = {
background: 'transparent', background: 'transparent',
@@ -68,29 +69,6 @@ function randomHex(length) {
return bytesToHex(bytes); return bytesToHex(bytes);
} }
const ColoredIcon = ({ src, color, size = '20px' }) => (
<div style={{
width: size,
height: size,
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
}}>
<img
src={src}
alt=""
style={{
width: size,
height: size,
transform: 'translateX(-1000px)',
filter: `drop-shadow(1000px 0 0 ${color})`
}}
/>
</div>
);
const VoiceTimer = () => { const VoiceTimer = () => {
const [elapsed, setElapsed] = React.useState(0); const [elapsed, setElapsed] = React.useState(0);
React.useEffect(() => { React.useEffect(() => {
@@ -114,7 +92,7 @@ const STATUS_OPTIONS = [
{ value: 'invisible', label: 'Invisible', color: '#747f8d' }, { value: 'invisible', label: 'Invisible', color: '#747f8d' },
]; ];
const UserControlPanel = ({ username, userId }) => { const UserControlPanel = React.memo(({ username, userId }) => {
const { session, idle } = usePlatform(); const { session, idle } = usePlatform();
const { isMuted, isDeafened, toggleMute, toggleDeafen, connectionState, disconnectVoice } = useVoice(); const { isMuted, isDeafened, toggleMute, toggleDeafen, connectionState, disconnectVoice } = useVoice();
const [showStatusMenu, setShowStatusMenu] = useState(false); const [showStatusMenu, setShowStatusMenu] = useState(false);
@@ -124,23 +102,32 @@ const UserControlPanel = ({ username, userId }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const manualStatusRef = useRef(false); const manualStatusRef = useRef(false);
const preIdleStatusRef = useRef('online'); const preIdleStatusRef = useRef('online');
const hasInitializedRef = useRef(false);
const currentStatusRef = useRef(currentStatus);
currentStatusRef.current = currentStatus;
// Fetch stored status preference from server and sync local state // Fetch stored status preference from server and sync local state
const allUsers = useQuery(api.auth.getPublicKeys) || []; const allUsers = useQuery(api.auth.getPublicKeys) || [];
const myUser = allUsers.find(u => u.id === userId); const myUser = allUsers.find(u => u.id === userId);
React.useEffect(() => { React.useEffect(() => {
if (myUser) { if (myUser) {
if (myUser.status && myUser.status !== 'offline') { const isInitial = !hasInitializedRef.current;
setCurrentStatus(myUser.status); if (isInitial) hasInitializedRef.current = true;
// dnd/invisible are manual overrides; idle is auto-set so don't count it
manualStatusRef.current = (myUser.status === 'dnd' || myUser.status === 'invisible'); // 'idle' is auto-set by the idle detector, not a user preference —
} else if (!myUser.status || myUser.status === 'offline') { // on a fresh app launch, reset it to 'online' just like 'offline'
// First login or no preference set yet — default to "online" const shouldReset = !myUser.status || myUser.status === 'offline'
|| (isInitial && myUser.status === 'idle');
if (shouldReset) {
setCurrentStatus('online'); setCurrentStatus('online');
manualStatusRef.current = false; manualStatusRef.current = false;
if (userId) { if (userId) {
updateStatusMutation({ userId, status: 'online' }).catch(() => {}); updateStatusMutation({ userId, status: 'online' }).catch(() => {});
} }
} else if (myUser.status) {
setCurrentStatus(myUser.status);
manualStatusRef.current = (myUser.status === 'dnd' || myUser.status === 'invisible');
} }
} }
}, [myUser?.status]); }, [myUser?.status]);
@@ -188,13 +175,13 @@ const UserControlPanel = ({ username, userId }) => {
} }
}; };
// Auto-idle detection via Electron powerMonitor // Auto-idle detection via platform idle API
useEffect(() => { useEffect(() => {
if (!idle || !userId) return; if (!idle || !userId) return;
const handleIdleChange = (data) => { const handleIdleChange = (data) => {
if (manualStatusRef.current) return; if (manualStatusRef.current) return;
if (data.isIdle) { if (data.isIdle) {
preIdleStatusRef.current = currentStatus; preIdleStatusRef.current = currentStatusRef.current;
setCurrentStatus('idle'); setCurrentStatus('idle');
updateStatusMutation({ userId, status: 'idle' }).catch(() => {}); updateStatusMutation({ userId, status: 'idle' }).catch(() => {});
} else { } else {
@@ -294,7 +281,7 @@ const UserControlPanel = ({ username, userId }) => {
)} )}
</div> </div>
); );
}; });
@@ -311,9 +298,9 @@ const voicePanelButtonStyle = {
flex: 1, flex: 1,
alignItems: 'center', alignItems: 'center',
minHeight: '32px', minHeight: '32px',
background: 'hsl(240 calc(1*4%) 60.784% /0.0784313725490196)', background: 'hsla(240, 4%, 60.784%, 0.078)',
border: 'hsl(0 calc(1*0%) 100% /0.0784313725490196)', border: 'hsla(0, 0%, 100%, 0.078)',
borderColor: 'color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.0392156862745098) 100%,hsl(0 0% 0% /0.0392156862745098) 0%)', borderColor: 'hsla(240, 4%, 60.784%, 0.039)',
borderRadius: '8px', borderRadius: '8px',
cursor: 'pointer', cursor: 'pointer',
padding: '4px', padding: '4px',
@@ -332,7 +319,7 @@ const liveBadgeStyle = {
height: '16px', height: '16px',
minHeight: '16px', minHeight: '16px',
minWidth: '16px', minWidth: '16px',
color: 'hsl(0 calc(1*0%) 100% /1)', color: 'hsl(0, 0%, 100%)',
fontSize: '12px', fontSize: '12px',
fontWeight: '700', fontWeight: '700',
letterSpacing: '.02em', letterSpacing: '.02em',
@@ -343,8 +330,8 @@ const liveBadgeStyle = {
marginRight: '4px' marginRight: '4px'
}; };
const ACTIVE_SPEAKER_SHADOW = '0 0 0 0px hsl(134.526 calc(1*41.485%) 44.902% /1), inset 0 0 0 2px hsl(134.526 calc(1*41.485%) 44.902% /1), inset 0 0 0 3px color-mix(in oklab, hsl(240 calc(1*7.143%) 10.98% /1) 100%, #000 0%)'; const ACTIVE_SPEAKER_SHADOW = '0 0 0 0px hsl(134.526, 41.485%, 44.902%), inset 0 0 0 2px hsl(134.526, 41.485%, 44.902%), inset 0 0 0 3px hsl(240, 7.143%, 10.98%)';
const VOICE_ACTIVE_COLOR = "color-mix(in oklab, hsl(132.809 calc(1*34.902%) 50% /1) 100%, #000 0%)"; const VOICE_ACTIVE_COLOR = 'hsl(132.809, 34.902%, 50%)';
async function encryptKeyForUsers(convex, channelId, keyHex, crypto) { async function encryptKeyForUsers(convex, channelId, keyHex, crypto) {
const users = await convex.query(api.auth.getPublicKeys, {}); const users = await convex.query(api.auth.getPublicKeys, {});
@@ -389,7 +376,7 @@ function getScreenCaptureConstraints(selection) {
}; };
} }
const VoiceUserContextMenu = ({ x, y, onClose, user, onMute, isMuted, onServerMute, isServerMuted, hasPermission, onMessage, isSelf, userVolume, onVolumeChange }) => { const VoiceUserContextMenu = ({ x, y, onClose, user, onMute, isMuted, onServerMute, isServerMuted, hasPermission, onDisconnect, hasDisconnectPermission, onMessage, isSelf, userVolume, onVolumeChange }) => {
const menuRef = useRef(null); const menuRef = useRef(null);
const [pos, setPos] = useState({ top: y, left: x }); const [pos, setPos] = useState({ top: y, left: x });
@@ -428,7 +415,7 @@ const VoiceUserContextMenu = ({ x, y, onClose, user, onMute, isMuted, onServerMu
value={userVolume} value={userVolume}
onChange={(e) => onVolumeChange(Number(e.target.value))} onChange={(e) => onVolumeChange(Number(e.target.value))}
className="context-menu-volume-slider" className="context-menu-volume-slider"
style={{ background: `linear-gradient(to right, hsl(235 86% 65%) ${sliderPercent}%, var(--bg-tertiary) ${sliderPercent}%)` }} style={{ background: `linear-gradient(to right, hsl(235, 86%, 65%) ${sliderPercent}%, var(--bg-tertiary) ${sliderPercent}%)` }}
/> />
</div> </div>
<div className="context-menu-separator" /> <div className="context-menu-separator" />
@@ -479,6 +466,14 @@ const VoiceUserContextMenu = ({ x, y, onClose, user, onMute, isMuted, onServerMu
</div> </div>
</div> </div>
)} )}
{!isSelf && hasDisconnectPermission && (
<div
className="context-menu-item"
onClick={(e) => { e.stopPropagation(); onDisconnect(); onClose(); }}
>
<span style={{ color: SERVER_MUTE_RED }}>Disconnect</span>
</div>
)}
<div className="context-menu-separator" /> <div className="context-menu-separator" />
<div className="context-menu-item" onClick={(e) => { e.stopPropagation(); onMessage(); onClose(); }}> <div className="context-menu-item" onClick={(e) => { e.stopPropagation(); onMessage(); onClose(); }}>
<span>Message</span> <span>Message</span>
@@ -754,7 +749,7 @@ const DraggableVoiceUser = ({ userId, channelId, disabled, children }) => {
); );
}; };
const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, username, channelKeys, view, onViewChange, onOpenDM, activeDMChannel, setActiveDMChannel, dmChannels, userId, serverName = 'Secure Chat', serverIconUrl }) => { const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, username, channelKeys, view, onViewChange, onOpenDM, activeDMChannel, setActiveDMChannel, dmChannels, userId, serverName = 'Secure Chat', serverIconUrl, isMobile }) => {
const { crypto, settings } = usePlatform(); const { crypto, settings } = usePlatform();
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [isServerSettingsOpen, setIsServerSettingsOpen] = useState(false); const [isServerSettingsOpen, setIsServerSettingsOpen] = useState(false);
@@ -829,6 +824,8 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
[dmChannels, unreadChannels, view, activeDMChannel] [dmChannels, unreadChannels, view, activeDMChannel]
); );
const { connectToVoice, activeChannelId: voiceChannelId, connectionState, disconnectVoice, activeChannelName: voiceChannelName, voiceStates, room, activeSpeakers, setScreenSharing, isPersonallyMuted, togglePersonalMute, isMuted: selfMuted, toggleMute, serverMute, disconnectUser, isServerMuted, serverSettings, getUserVolume, setUserVolume, isReceivingScreenShareAudio } = useVoice();
const prevUnreadDMsRef = useRef(null); const prevUnreadDMsRef = useRef(null);
useEffect(() => { useEffect(() => {
@@ -843,15 +840,17 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
for (const id of currentIds) { for (const id of currentIds) {
if (!prevUnreadDMsRef.current.has(id)) { if (!prevUnreadDMsRef.current.has(id)) {
if (!isReceivingScreenShareAudio) {
const audio = new Audio(PingSound); const audio = new Audio(PingSound);
audio.volume = 0.5; audio.volume = 0.5;
audio.play().catch(() => {}); audio.play().catch(() => {});
}
break; break;
} }
} }
prevUnreadDMsRef.current = currentIds; prevUnreadDMsRef.current = currentIds;
}, [dmChannels, unreadChannels]); }, [dmChannels, unreadChannels, isReceivingScreenShareAudio]);
const onRenameChannel = () => {}; const onRenameChannel = () => {};
@@ -859,8 +858,6 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
if (activeChannel === id) onSelectChannel(null); if (activeChannel === id) onSelectChannel(null);
}; };
const { connectToVoice, activeChannelId: voiceChannelId, connectionState, disconnectVoice, activeChannelName: voiceChannelName, voiceStates, room, activeSpeakers, setScreenSharing, isPersonallyMuted, togglePersonalMute, isMuted: selfMuted, toggleMute, serverMute, isServerMuted, serverSettings, getUserVolume, setUserVolume } = useVoice();
const handleStartCreate = () => { const handleStartCreate = () => {
setIsCreating(true); setIsCreating(true);
setNewChannelName(''); setNewChannelName('');
@@ -987,7 +984,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
}); });
} }
new Audio(screenShareStartSound).play(); if (!isReceivingScreenShareAudio) new Audio(screenShareStartSound).play();
setScreenSharing(true); setScreenSharing(true);
track.onended = () => { track.onended = () => {
@@ -1017,7 +1014,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
} }
} }
room.localParticipant.setScreenShareEnabled(false); room.localParticipant.setScreenShareEnabled(false);
new Audio(screenShareStopSound).play(); if (!isReceivingScreenShareAudio) new Audio(screenShareStopSound).play();
setScreenSharing(false); setScreenSharing(false);
} else { } else {
setIsScreenShareModalOpen(true); setIsScreenShareModalOpen(true);
@@ -1025,8 +1022,11 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
}; };
const handleChannelClick = (channel) => { const handleChannelClick = (channel) => {
if (channel.type === 'voice' && voiceChannelId !== channel._id) { if (channel.type === 'voice') {
if (voiceChannelId !== channel._id) {
connectToVoice(channel._id, channel.name, localStorage.getItem('userId')); connectToVoice(channel._id, channel.name, localStorage.getItem('userId'));
}
onSelectChannel(channel._id);
} else { } else {
onSelectChannel(channel._id); onSelectChannel(channel._id);
} }
@@ -1127,11 +1127,18 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
); );
}; };
const toggleCategory = (cat) => { const toggleCategory = useCallback((cat) => {
const next = { ...collapsedCategories, [cat]: !collapsedCategories[cat] }; setCollapsedCategories(prev => {
setCollapsedCategories(next); const next = { ...prev, [cat]: !prev[cat] };
setUserPref(userId, 'collapsedCategories', next, settings); setUserPref(userId, 'collapsedCategories', next, settings);
}; return next;
});
}, [userId, settings]);
const handleAddChannelToCategory = useCallback((groupId) => {
setCreateChannelCategoryId(groupId === '__uncategorized__' ? null : groupId);
setShowCreateChannelModal(true);
}, []);
// Group channels by categoryId // Group channels by categoryId
const groupedChannels = React.useMemo(() => { const groupedChannels = React.useMemo(() => {
@@ -1377,12 +1384,10 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
<SortableCategory key={group.id} id={`category-${group.id}`}> <SortableCategory key={group.id} id={`category-${group.id}`}>
<CategoryHeader <CategoryHeader
group={group} group={group}
groupId={group.id}
collapsed={collapsedCategories[group.id]} collapsed={collapsedCategories[group.id]}
onToggle={() => toggleCategory(group.id)} onToggle={toggleCategory}
onAddChannel={() => { onAddChannel={handleAddChannelToCategory}
setCreateChannelCategoryId(group.id === '__uncategorized__' ? null : group.id);
setShowCreateChannelModal(true);
}}
/> />
{(() => { {(() => {
const isCollapsed = collapsedCategories[group.id]; const isCollapsed = collapsedCategories[group.id];
@@ -1578,7 +1583,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
margin: '8px 8px 0px 8px', margin: '8px 8px 0px 8px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
borderBottom: "1px solid color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.0392156862745098) 100%,hsl(0 0% 0% /0.0392156862745098) 0%)" borderBottom: '1px solid hsla(240, 4%, 60.784%, 0.039)'
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 4 }}>
<div style={{ color: '#43b581', fontWeight: 'bold', fontSize: 13 }}>Voice Connected</div> <div style={{ color: '#43b581', fontWeight: 'bold', fontSize: 13 }}>Voice Connected</div>
@@ -1648,6 +1653,8 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
isServerMuted={isServerMuted(voiceUserMenu.user.userId)} isServerMuted={isServerMuted(voiceUserMenu.user.userId)}
onServerMute={() => serverMute(voiceUserMenu.user.userId, !isServerMuted(voiceUserMenu.user.userId))} onServerMute={() => serverMute(voiceUserMenu.user.userId, !isServerMuted(voiceUserMenu.user.userId))}
hasPermission={!!myPermissions.mute_members} hasPermission={!!myPermissions.mute_members}
onDisconnect={() => disconnectUser(voiceUserMenu.user.userId)}
hasDisconnectPermission={!!myPermissions.move_members}
onMessage={() => { onMessage={() => {
onOpenDM(voiceUserMenu.user.userId, voiceUserMenu.user.username); onOpenDM(voiceUserMenu.user.userId, voiceUserMenu.user.username);
onViewChange('me'); onViewChange('me');
@@ -1692,16 +1699,16 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
}; };
// Category header component (extracted for DnD drag handle) // Category header component (extracted for DnD drag handle)
const CategoryHeader = ({ group, collapsed, onToggle, onAddChannel, dragListeners }) => ( const CategoryHeader = React.memo(({ group, groupId, collapsed, onToggle, onAddChannel, dragListeners }) => (
<div className="channel-category-header" onClick={onToggle} {...(dragListeners || {})}> <div className="channel-category-header" onClick={() => onToggle(groupId)} {...(dragListeners || {})}>
<span className="category-label">{group.name}</span> <span className="category-label">{group.name}</span>
<div className={`category-chevron ${collapsed ? 'collapsed' : ''}`}> <div className={`category-chevron ${collapsed ? 'collapsed' : ''}`}>
<ColoredIcon src={categoryCollapsedIcon} color="currentColor" size="12px" /> <ColoredIcon src={categoryCollapsedIcon} color="currentColor" size="12px" />
</div> </div>
<button className="category-add-btn" onClick={(e) => { e.stopPropagation(); onAddChannel(); }} title="Create Channel"> <button className="category-add-btn" onClick={(e) => { e.stopPropagation(); onAddChannel(groupId); }} title="Create Channel">
+ +
</button> </button>
</div> </div>
); ));
export default Sidebar; export default Sidebar;

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect, createContext, useContext } from 'react'; import React, { useState, useEffect, createContext, useContext } from 'react';
import { usePlatform } from '../platform'; import { usePlatform } from '../platform';
import ColoredIcon from './ColoredIcon';
import updateIcon from '../assets/icons/update.svg'; import updateIcon from '../assets/icons/update.svg';
const RELEASE_URL = 'https://gitea.moyettes.com/Moyettes/DiscordClone/releases/tag/latest'; const RELEASE_URL = 'https://gitea.moyettes.com/Moyettes/DiscordClone/releases/tag/latest';
@@ -73,26 +74,7 @@ export function TitleBarUpdateIcon() {
style={{ borderRight: '1px solid var(--app-frame-border)' }} style={{ borderRight: '1px solid var(--app-frame-border)' }}
> >
<div style={{ marginRight: '12px' }}> <div style={{ marginRight: '12px' }}>
<div style={{ <ColoredIcon src={updateIcon} color="#3ba55c" size="20px" />
width: 20,
height: 20,
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
}}>
<img
src={updateIcon}
alt=""
style={{
width: 20,
height: 20,
transform: 'translateX(-1000px)',
filter: `drop-shadow(1000px 0 0 #3ba55c)`,
}}
/>
</div>
</div> </div>
</button> </button>
); );

View File

@@ -15,8 +15,9 @@ import personalMuteIcon from '../assets/icons/personal_mute.svg';
import serverMuteIcon from '../assets/icons/server_mute.svg'; import serverMuteIcon from '../assets/icons/server_mute.svg';
import screenShareStartSound from '../assets/sounds/screenshare_start.mp3'; import screenShareStartSound from '../assets/sounds/screenshare_start.mp3';
import screenShareStopSound from '../assets/sounds/screenshare_stop.mp3'; import screenShareStopSound from '../assets/sounds/screenshare_stop.mp3';
import ColoredIcon from './ColoredIcon';
const SERVER_MUTE_RED = 'color-mix(in oklab, hsl(1.343 calc(1*84.81%) 69.02% /1) 100%, #000 0%)'; const SERVER_MUTE_RED = 'hsl(1.343, 84.81%, 69.02%)';
const getInitials = (name) => (name || '?').substring(0, 1).toUpperCase(); const getInitials = (name) => (name || '?').substring(0, 1).toUpperCase();
@@ -43,30 +44,6 @@ const WATCH_STREAM_BUTTON_STYLE = {
const THUMBNAIL_SIZE = { width: 120, height: 68 }; const THUMBNAIL_SIZE = { width: 120, height: 68 };
const BOTTOM_BAR_HEIGHT = 140; const BOTTOM_BAR_HEIGHT = 140;
// Helper Component for coloring SVGs (Reused from Sidebar)
const ColoredIcon = ({ src, color, size = '24px' }) => (
<div style={{
width: size,
height: size,
overflow: 'hidden',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0
}}>
<img
src={src}
alt=""
style={{
width: size,
height: size,
transform: 'translateX(-1000px)',
filter: `drop-shadow(1000px 0 0 ${color})`
}}
/>
</div>
);
// --- Components --- // --- Components ---
const ParticipantTile = ({ participant, username, avatarUrl }) => { const ParticipantTile = ({ participant, username, avatarUrl }) => {
@@ -266,6 +243,47 @@ const ParticipantThumbnail = ({ participant, username, avatarUrl, isStreamer, is
); );
}; };
// Inline SVG icons for volume control
const SpeakerIcon = ({ volume, muted }) => {
if (muted || volume === 0) {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="white">
<path d="M11 5L6 9H2v6h4l5 4V5z" />
<line x1="23" y1="9" x2="17" y2="15" stroke="white" strokeWidth="2" strokeLinecap="round" />
<line x1="17" y1="9" x2="23" y2="15" stroke="white" strokeWidth="2" strokeLinecap="round" />
</svg>
);
}
if (volume < 50) {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="white">
<path d="M11 5L6 9H2v6h4l5 4V5z" />
<path d="M15.54 8.46a5 5 0 010 7.07" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" />
</svg>
);
}
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="white">
<path d="M11 5L6 9H2v6h4l5 4V5z" />
<path d="M15.54 8.46a5 5 0 010 7.07" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" />
<path d="M19.07 4.93a10 10 0 010 14.14" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" />
</svg>
);
};
// Inline SVG icons for fullscreen
const ExpandIcon = () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="white">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
</svg>
);
const CompressIcon = () => (
<svg width="18" height="18" viewBox="0 0 24 24" fill="white">
<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z" />
</svg>
);
const FocusedStreamView = ({ const FocusedStreamView = ({
streamParticipant, streamParticipant,
streamerUsername, streamerUsername,
@@ -278,11 +296,34 @@ const FocusedStreamView = ({
streamingIdentities, streamingIdentities,
voiceUsers, voiceUsers,
isTabVisible, isTabVisible,
isFullscreen,
onToggleFullscreen,
localIdentity,
}) => { }) => {
const screenTrack = useParticipantTrack(streamParticipant, 'screenshare'); const screenTrack = useParticipantTrack(streamParticipant, 'screenshare');
const [barHover, setBarHover] = useState(false); const [barHover, setBarHover] = useState(false);
const [bottomEdgeHover, setBottomEdgeHover] = useState(false); const [bottomEdgeHover, setBottomEdgeHover] = useState(false);
// Volume control state
const { getUserVolume, setUserVolume, togglePersonalMute, isPersonallyMuted } = useVoice();
const streamerId = streamParticipant.identity;
const isSelf = streamerId === localIdentity;
const isMutedByMe = isPersonallyMuted(streamerId);
const userVolume = getUserVolume(streamerId);
const [volumeExpanded, setVolumeExpanded] = useState(false);
const volumeHideTimeout = useRef(null);
const handleVolumeMouseEnter = () => {
if (volumeHideTimeout.current) clearTimeout(volumeHideTimeout.current);
setVolumeExpanded(true);
};
const handleVolumeMouseLeave = () => {
volumeHideTimeout.current = setTimeout(() => setVolumeExpanded(false), 1500);
};
useEffect(() => () => { if (volumeHideTimeout.current) clearTimeout(volumeHideTimeout.current); }, []);
const sliderPercent = (userVolume / 200) * 100;
// Auto-exit if stream track disappears // Auto-exit if stream track disappears
useEffect(() => { useEffect(() => {
if (!streamParticipant) { if (!streamParticipant) {
@@ -362,12 +403,30 @@ const FocusedStreamView = ({
<span style={LIVE_BADGE_STYLE}>LIVE</span> <span style={LIVE_BADGE_STYLE}>LIVE</span>
</div> </div>
{/* Top-right: close button */} {/* Top-right: button group (fullscreen + close) */}
<div style={{
position: 'absolute', top: '12px', right: '12px',
display: 'flex', gap: '8px',
}}>
<button
onClick={onToggleFullscreen}
title={isFullscreen ? "Exit Fullscreen" : "Fullscreen"}
style={{
width: '32px', height: '32px', borderRadius: '50%',
backgroundColor: 'rgba(0,0,0,0.6)', border: 'none',
color: 'white', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'background-color 0.15s',
}}
onMouseEnter={e => e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.8)'}
onMouseLeave={e => e.currentTarget.style.backgroundColor = 'rgba(0,0,0,0.6)'}
>
{isFullscreen ? <CompressIcon /> : <ExpandIcon />}
</button>
<button <button
onClick={onStopWatching} onClick={onStopWatching}
title="Stop Watching" title="Stop Watching"
style={{ style={{
position: 'absolute', top: '12px', right: '12px',
width: '32px', height: '32px', borderRadius: '50%', width: '32px', height: '32px', borderRadius: '50%',
backgroundColor: 'rgba(0,0,0,0.6)', border: 'none', backgroundColor: 'rgba(0,0,0,0.6)', border: 'none',
color: 'white', fontSize: '18px', cursor: 'pointer', color: 'white', fontSize: '18px', cursor: 'pointer',
@@ -381,6 +440,55 @@ const FocusedStreamView = ({
</button> </button>
</div> </div>
{/* Bottom-left: volume control (hidden when watching own stream) */}
{!isSelf && (
<div
onMouseEnter={handleVolumeMouseEnter}
onMouseLeave={handleVolumeMouseLeave}
style={{
position: 'absolute', bottom: '12px', left: '12px',
display: 'flex', alignItems: 'center', gap: '8px',
backgroundColor: 'rgba(0,0,0,0.6)',
padding: '6px 10px',
borderRadius: '6px',
transition: 'width 0.2s ease',
}}
>
<button
onClick={() => togglePersonalMute(streamerId)}
title={isMutedByMe ? "Unmute" : "Mute"}
style={{
background: 'none', border: 'none', cursor: 'pointer',
padding: 0, display: 'flex', alignItems: 'center',
opacity: isMutedByMe ? 0.6 : 1,
}}
>
<SpeakerIcon volume={userVolume} muted={isMutedByMe} />
</button>
{volumeExpanded && (
<>
<input
type="range"
min="0"
max="200"
value={isMutedByMe ? 0 : userVolume}
onChange={(e) => setUserVolume(streamerId, Number(e.target.value))}
onMouseDown={(e) => e.stopPropagation()}
className="context-menu-volume-slider"
style={{
width: '100px',
background: `linear-gradient(to right, hsl(235, 86%, 65%) ${isMutedByMe ? 0 : sliderPercent}%, rgba(255,255,255,0.2) ${isMutedByMe ? 0 : sliderPercent}%)`,
}}
/>
<span style={{ color: 'white', fontSize: '12px', minWidth: '32px', textAlign: 'right' }}>
{isMutedByMe ? 0 : userVolume}%
</span>
</>
)}
</div>
)}
</div>
{/* Bottom participants bar */} {/* Bottom participants bar */}
<div <div
style={{ position: 'relative' }} style={{ position: 'relative' }}
@@ -473,13 +581,37 @@ const FocusedStreamView = ({
const VoiceStage = ({ room, channelId, voiceStates, channelName }) => { const VoiceStage = ({ room, channelId, voiceStates, channelName }) => {
const [participants, setParticipants] = useState([]); const [participants, setParticipants] = useState([]);
const { isMuted, toggleMute, disconnectVoice, setScreenSharing, connectToVoice, watchingStreamOf, setWatchingStreamOf } = useVoice(); const { isMuted, toggleMute, disconnectVoice, setScreenSharing, connectToVoice, watchingStreamOf, setWatchingStreamOf, isReceivingScreenShareAudio } = useVoice();
const [isScreenShareModalOpen, setIsScreenShareModalOpen] = useState(false); const [isScreenShareModalOpen, setIsScreenShareModalOpen] = useState(false);
const [isScreenShareActive, setIsScreenShareActive] = useState(false); const [isScreenShareActive, setIsScreenShareActive] = useState(false);
const screenShareAudioTrackRef = useRef(null); const screenShareAudioTrackRef = useRef(null);
const [participantsCollapsed, setParticipantsCollapsed] = useState(false); const [participantsCollapsed, setParticipantsCollapsed] = useState(false);
// Fullscreen support
const stageContainerRef = useRef(null);
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
}, []);
const toggleFullscreen = useCallback(() => {
if (!stageContainerRef.current) return;
if (document.fullscreenElement) {
document.exitFullscreen().catch(console.error);
} else {
stageContainerRef.current.requestFullscreen().catch(console.error);
}
}, []);
const isReceivingScreenShareAudioRef = useRef(false);
useEffect(() => { isReceivingScreenShareAudioRef.current = isReceivingScreenShareAudio; }, [isReceivingScreenShareAudio]);
useEffect(() => { useEffect(() => {
if (!room) return; if (!room) return;
@@ -496,7 +628,7 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => {
room.on(RoomEvent.ParticipantDisconnected, updateParticipants); room.on(RoomEvent.ParticipantDisconnected, updateParticipants);
room.localParticipant.on('localTrackPublished', updateParticipants); room.localParticipant.on('localTrackPublished', updateParticipants);
room.localParticipant.on('localTrackUnpublished', (pub) => { room.localParticipant.on('localTrackUnpublished', (pub) => {
if (pub.source === Track.Source.ScreenShare || pub.source === 'screen_share') { if ((pub.source === Track.Source.ScreenShare || pub.source === 'screen_share') && !isReceivingScreenShareAudioRef.current) {
new Audio(screenShareStopSound).play(); new Audio(screenShareStopSound).play();
} }
updateParticipants(); updateParticipants();
@@ -510,10 +642,11 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => {
}; };
}, [room]); }, [room]);
// Reset collapsed state when room disconnects // Reset collapsed state and exit fullscreen when room disconnects
useEffect(() => { useEffect(() => {
if (!room) { if (!room) {
setParticipantsCollapsed(false); setParticipantsCollapsed(false);
if (document.fullscreenElement) document.exitFullscreen().catch(console.error);
} }
}, [room]); }, [room]);
@@ -524,6 +657,7 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => {
); );
const handleStopWatching = useCallback(() => { const handleStopWatching = useCallback(() => {
if (document.fullscreenElement) document.exitFullscreen().catch(console.error);
setWatchingStreamOf(null); setWatchingStreamOf(null);
setParticipantsCollapsed(false); setParticipantsCollapsed(false);
}, []); }, []);
@@ -589,7 +723,7 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => {
} }
const track = stream.getVideoTracks()[0]; const track = stream.getVideoTracks()[0];
if (track) { if (track) {
new Audio(screenShareStartSound).play(); if (!isReceivingScreenShareAudio) new Audio(screenShareStartSound).play();
await room.localParticipant.publishTrack(track, { await room.localParticipant.publishTrack(track, {
name: 'screen_share', name: 'screen_share',
source: Track.Source.ScreenShare source: Track.Source.ScreenShare
@@ -657,6 +791,21 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => {
return () => document.removeEventListener('visibilitychange', handler); return () => document.removeEventListener('visibilitychange', handler);
}, []); }, []);
// F key shortcut to toggle fullscreen when watching a stream
useEffect(() => {
if (!watchingStreamOf) return;
const handleKeyDown = (e) => {
if (e.key === 'f' || e.key === 'F') {
const tag = e.target.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || e.target.isContentEditable) return;
e.preventDefault();
toggleFullscreen();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [watchingStreamOf, toggleFullscreen]);
if (!room) { if (!room) {
return ( return (
<div style={{ <div style={{
@@ -732,7 +881,7 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => {
: null; : null;
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', flex: 1, backgroundColor: 'black', width: '100%' }}> <div ref={stageContainerRef} style={{ display: 'flex', flexDirection: 'column', height: '100%', flex: 1, backgroundColor: 'black', width: '100%' }}>
{watchingStreamOf && watchedParticipant ? ( {watchingStreamOf && watchedParticipant ? (
/* Focused/Fullscreen View */ /* Focused/Fullscreen View */
<FocusedStreamView <FocusedStreamView
@@ -747,6 +896,9 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => {
streamingIdentities={streamingIdentities} streamingIdentities={streamingIdentities}
voiceUsers={voiceUsers} voiceUsers={voiceUsers}
isTabVisible={isTabVisible} isTabVisible={isTabVisible}
isFullscreen={isFullscreen}
onToggleFullscreen={toggleFullscreen}
localIdentity={room.localParticipant.identity}
/> />
) : ( ) : (
/* Grid View */ /* Grid View */

View File

@@ -1,5 +1,5 @@
import React, { createContext, useContext, useMemo } from 'react'; import React, { createContext, useContext, useMemo } from 'react';
import usePresence from '@convex-dev/presence/react'; import usePresence from '../hooks/usePresence.js';
import { api } from '../../../../convex/_generated/api'; import { api } from '../../../../convex/_generated/api';
const PresenceContext = createContext({ const PresenceContext = createContext({

View File

@@ -31,7 +31,10 @@ const VoiceContext = createContext();
export const useVoice = () => useContext(VoiceContext); export const useVoice = () => useContext(VoiceContext);
let _suppressAppSounds = false;
function playSound(type) { function playSound(type) {
if (_suppressAppSounds) return;
const src = soundMap[type]; const src = soundMap[type];
if (!src) return; if (!src) return;
const audio = new Audio(src); const audio = new Audio(src);
@@ -40,6 +43,7 @@ function playSound(type) {
} }
function playSoundUrl(url) { function playSoundUrl(url) {
if (_suppressAppSounds) return;
const audio = new Audio(url); const audio = new Audio(url);
audio.volume = 0.5; audio.volume = 0.5;
audio.play().catch(e => console.error("Sound play failed", e)); audio.play().catch(e => console.error("Sound play failed", e));
@@ -60,6 +64,7 @@ export const VoiceProvider = ({ children }) => {
parseInt(localStorage.getItem('voiceOutputVolume') || '100') parseInt(localStorage.getItem('voiceOutputVolume') || '100')
); );
const isMovingRef = useRef(false); const isMovingRef = useRef(false);
const [isReceivingScreenShareAudio, setIsReceivingScreenShareAudio] = useState(false);
const convex = useConvex(); const convex = useConvex();
@@ -113,7 +118,7 @@ export const VoiceProvider = ({ children }) => {
// Apply volume to LiveKit participant (factoring in global output volume) // Apply volume to LiveKit participant (factoring in global output volume)
const participant = room?.remoteParticipants?.get(userId); const participant = room?.remoteParticipants?.get(userId);
const globalVol = globalOutputVolume / 100; const globalVol = globalOutputVolume / 100;
if (participant) participant.setVolume((volume / 100) * globalVol); if (participant) participant.setVolume(Math.min(1, (volume / 100) * globalVol));
// Sync personal mute state // Sync personal mute state
if (volume === 0) { if (volume === 0) {
setPersonallyMutedUsers(prev => { setPersonallyMutedUsers(prev => {
@@ -147,7 +152,7 @@ export const VoiceProvider = ({ children }) => {
const vol = userVolumes[userId] ?? 100; const vol = userVolumes[userId] ?? 100;
const restoreVol = vol === 0 ? 100 : vol; const restoreVol = vol === 0 ? 100 : vol;
const participant = room?.remoteParticipants?.get(userId); const participant = room?.remoteParticipants?.get(userId);
if (participant) participant.setVolume((restoreVol / 100) * globalVol); if (participant) participant.setVolume(Math.min(1, (restoreVol / 100) * globalVol));
// Update stored volume if it was 0 // Update stored volume if it was 0
if (vol === 0) { if (vol === 0) {
setUserVolumes(p => { setUserVolumes(p => {
@@ -178,6 +183,16 @@ export const VoiceProvider = ({ children }) => {
} }
}; };
const disconnectUser = async (targetUserId) => {
const actorUserId = localStorage.getItem('userId');
if (!actorUserId) return;
try {
await convex.mutation(api.voiceState.disconnectUser, { actorUserId, targetUserId });
} catch (e) {
console.error('Failed to disconnect user:', e);
}
};
const isServerMuted = (userId) => { const isServerMuted = (userId) => {
for (const users of Object.values(voiceStates)) { for (const users of Object.values(voiceStates)) {
const user = users.find(u => u.userId === userId); const user = users.find(u => u.userId === userId);
@@ -197,7 +212,7 @@ export const VoiceProvider = ({ children }) => {
); );
// Refs for detecting other-user joins via voiceStates changes // Refs for detecting other-user joins via voiceStates changes
const prevChannelUsersRef = useRef(new Map()); const prevChannelUsersRef = useRef(new Set());
const otherJoinInitRef = useRef(false); const otherJoinInitRef = useRef(false);
const isInAfkChannel = !!(activeChannelId && serverSettings?.afkChannelId === activeChannelId); const isInAfkChannel = !!(activeChannelId && serverSettings?.afkChannelId === activeChannelId);
@@ -378,7 +393,7 @@ export const VoiceProvider = ({ children }) => {
participant.setVolume(0); participant.setVolume(0);
} else { } else {
const userVol = (userVolumes[identity] ?? 100) / 100; const userVol = (userVolumes[identity] ?? 100) / 100;
participant.setVolume(userVol * globalVol); participant.setVolume(Math.min(1, userVol * globalVol));
} }
} }
}; };
@@ -418,31 +433,34 @@ export const VoiceProvider = ({ children }) => {
// Detect other users joining the same voice channel and play their join sound // Detect other users joining the same voice channel and play their join sound
useEffect(() => { useEffect(() => {
if (!activeChannelId) { if (!activeChannelId) {
prevChannelUsersRef.current = new Map(); prevChannelUsersRef.current = new Set();
otherJoinInitRef.current = false; otherJoinInitRef.current = false;
return; return;
} }
const selfId = localStorage.getItem('userId'); const selfId = localStorage.getItem('userId');
const channelUsers = voiceStates[activeChannelId] || []; const channelUsers = voiceStates[activeChannelId] || [];
const currentUsers = new Map(); const currentUserIds = new Set(channelUsers.map(u => u.userId));
for (const u of channelUsers) {
currentUsers.set(u.userId, u); // Guard: ignore transient empty states when we previously had users
if (currentUserIds.size === 0 && prevChannelUsersRef.current.size > 0) {
return;
} }
// Skip the first render after joining to avoid playing sounds for users already in the channel // Skip the first render after joining to avoid playing sounds for users already in the channel
if (!otherJoinInitRef.current) { if (!otherJoinInitRef.current) {
otherJoinInitRef.current = true; otherJoinInitRef.current = true;
prevChannelUsersRef.current = currentUsers; prevChannelUsersRef.current = currentUserIds;
return; return;
} }
const prev = prevChannelUsersRef.current; const prevIds = prevChannelUsersRef.current;
// Detect new users (not self) // Detect new users (not self)
for (const [uid, userData] of currentUsers) { for (const uid of currentUserIds) {
if (uid !== selfId && !prev.has(uid)) { if (uid !== selfId && !prevIds.has(uid)) {
if (userData.joinSoundUrl) { const userData = channelUsers.find(u => u.userId === uid);
if (userData?.joinSoundUrl) {
playSoundUrl(userData.joinSoundUrl); playSoundUrl(userData.joinSoundUrl);
} else { } else {
playSound('join'); playSound('join');
@@ -451,7 +469,7 @@ export const VoiceProvider = ({ children }) => {
} }
} }
prevChannelUsersRef.current = currentUsers; prevChannelUsersRef.current = currentUserIds;
}, [voiceStates, activeChannelId]); }, [voiceStates, activeChannelId]);
// Manage screen share subscriptions — only subscribe when actively watching // Manage screen share subscriptions — only subscribe when actively watching
@@ -459,6 +477,7 @@ export const VoiceProvider = ({ children }) => {
if (!room) return; if (!room) return;
const manageSubscriptions = () => { const manageSubscriptions = () => {
let receivingAudio = false;
for (const p of room.remoteParticipants.values()) { for (const p of room.remoteParticipants.values()) {
const { screenSharePub, screenShareAudioPub } = findTrackPubs(p); const { screenSharePub, screenShareAudioPub } = findTrackPubs(p);
@@ -470,7 +489,13 @@ export const VoiceProvider = ({ children }) => {
if (screenShareAudioPub && screenShareAudioPub.isSubscribed !== shouldSubscribe) { if (screenShareAudioPub && screenShareAudioPub.isSubscribed !== shouldSubscribe) {
screenShareAudioPub.setSubscribed(shouldSubscribe); screenShareAudioPub.setSubscribed(shouldSubscribe);
} }
if (shouldSubscribe && screenShareAudioPub && screenShareAudioPub.isSubscribed) {
receivingAudio = true;
} }
}
_suppressAppSounds = receivingAudio;
setIsReceivingScreenShareAudio(receivingAudio);
}; };
manageSubscriptions(); manageSubscriptions();
@@ -478,10 +503,14 @@ export const VoiceProvider = ({ children }) => {
const onTrackChange = () => manageSubscriptions(); const onTrackChange = () => manageSubscriptions();
room.on(RoomEvent.TrackPublished, onTrackChange); room.on(RoomEvent.TrackPublished, onTrackChange);
room.on(RoomEvent.TrackSubscribed, onTrackChange); room.on(RoomEvent.TrackSubscribed, onTrackChange);
room.on(RoomEvent.TrackUnsubscribed, onTrackChange);
return () => { return () => {
room.off(RoomEvent.TrackPublished, onTrackChange); room.off(RoomEvent.TrackPublished, onTrackChange);
room.off(RoomEvent.TrackSubscribed, onTrackChange); room.off(RoomEvent.TrackSubscribed, onTrackChange);
room.off(RoomEvent.TrackUnsubscribed, onTrackChange);
_suppressAppSounds = false;
setIsReceivingScreenShareAudio(false);
}; };
}, [room, watchingStreamOf]); }, [room, watchingStreamOf]);
@@ -639,6 +668,7 @@ export const VoiceProvider = ({ children }) => {
setUserVolume, setUserVolume,
getUserVolume, getUserVolume,
serverMute, serverMute,
disconnectUser,
isServerMuted, isServerMuted,
isInAfkChannel, isInAfkChannel,
serverSettings, serverSettings,
@@ -647,6 +677,7 @@ export const VoiceProvider = ({ children }) => {
switchDevice, switchDevice,
globalOutputVolume, globalOutputVolume,
setGlobalOutputVolume, setGlobalOutputVolume,
isReceivingScreenShareAudio,
}}> }}>
{children} {children}
{room && ( {room && (

View File

@@ -0,0 +1,18 @@
import { useState, useEffect } from 'react';
const MOBILE_BREAKPOINT = '(max-width: 768px)';
export function useIsMobile() {
const [isMobile, setIsMobile] = useState(() =>
typeof window !== 'undefined' && window.matchMedia(MOBILE_BREAKPOINT).matches
);
useEffect(() => {
const mql = window.matchMedia(MOBILE_BREAKPOINT);
const handler = (e) => setIsMobile(e.matches);
mql.addEventListener('change', handler);
return () => mql.removeEventListener('change', handler);
}, []);
return isMobile;
}

View File

@@ -0,0 +1,123 @@
/**
* Custom usePresence hook based on @convex-dev/presence/react.
*
* Fix: The upstream hook disconnects on `visibilitychange` (document.hidden),
* which marks the user offline when the Electron window is minimized.
* This version keeps heartbeats running when hidden and only sends an
* immediate heartbeat + restarts the interval when becoming visible again
* (in case the browser had throttled timers).
*/
import { useEffect, useMemo, useRef, useState } from "react";
import { useQuery, useMutation, useConvex } from "convex/react";
import useSingleFlight from "./useSingleFlight.js";
export default function usePresence(presence, roomId, userId, interval = 10000, convexUrl) {
const hasMounted = useRef(false);
const convex = useConvex();
const baseUrl = convexUrl ?? convex.url;
const [sessionId, setSessionId] = useState(() => crypto.randomUUID());
const [sessionToken, setSessionToken] = useState(null);
const sessionTokenRef = useRef(null);
const [roomToken, setRoomToken] = useState(null);
const roomTokenRef = useRef(null);
const intervalRef = useRef(null);
const heartbeat = useSingleFlight(useMutation(presence.heartbeat));
const disconnect = useSingleFlight(useMutation(presence.disconnect));
useEffect(() => {
// Reset session state when roomId or userId changes.
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (sessionTokenRef.current) {
void disconnect({ sessionToken: sessionTokenRef.current });
}
setSessionId(crypto.randomUUID());
setSessionToken(null);
setRoomToken(null);
}, [roomId, userId, disconnect]);
useEffect(() => {
sessionTokenRef.current = sessionToken;
roomTokenRef.current = roomToken;
}, [sessionToken, roomToken]);
useEffect(() => {
const sendHeartbeat = async () => {
const result = await heartbeat({ roomId, userId, sessionId, interval });
setRoomToken(result.roomToken);
setSessionToken(result.sessionToken);
};
// Send initial heartbeat
void sendHeartbeat();
// Clear any existing interval before setting a new one
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
intervalRef.current = setInterval(sendHeartbeat, interval);
// Handle page unload.
const handleUnload = () => {
if (sessionTokenRef.current) {
const blob = new Blob([
JSON.stringify({
path: "presence:disconnect",
args: { sessionToken: sessionTokenRef.current },
}),
], { type: "application/json" });
navigator.sendBeacon(`${baseUrl}/api/mutation`, blob);
}
};
window.addEventListener("beforeunload", handleUnload);
// Handle visibility changes.
// FIX: Do NOT disconnect when hidden. Electron timers keep running
// when minimized, so heartbeats continue normally. Only send an
// immediate heartbeat when becoming visible again to recover quickly
// in case the browser had throttled the interval.
const handleVisibility = async () => {
if (!document.hidden) {
void sendHeartbeat();
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
intervalRef.current = setInterval(sendHeartbeat, interval);
}
};
const wrappedHandleVisibility = () => {
handleVisibility().catch(console.error);
};
document.addEventListener("visibilitychange", wrappedHandleVisibility);
// Cleanup.
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
document.removeEventListener("visibilitychange", wrappedHandleVisibility);
window.removeEventListener("beforeunload", handleUnload);
// Don't disconnect on first render in strict mode.
if (hasMounted.current) {
if (sessionTokenRef.current) {
void disconnect({ sessionToken: sessionTokenRef.current });
}
}
};
}, [heartbeat, disconnect, roomId, userId, baseUrl, interval, sessionId]);
useEffect(() => {
hasMounted.current = true;
}, []);
const state = useQuery(presence.list, roomToken ? { roomToken } : "skip");
return useMemo(() => state?.slice().sort((a, b) => {
if (a.userId === userId) return -1;
if (b.userId === userId) return 1;
return 0;
}), [state, userId]);
}

View File

@@ -0,0 +1,40 @@
import { useCallback, useRef } from "react";
/**
* Wraps a function to single-flight invocations, using the latest args.
*
* Copied from @convex-dev/presence/dist/react/useSingleFlight.js
*/
export default function useSingleFlight(fn) {
const flightStatus = useRef({
inFlight: false,
upNext: null,
});
return useCallback((...args) => {
if (flightStatus.current.inFlight) {
return new Promise((resolve, reject) => {
flightStatus.current.upNext = { fn, resolve, reject, args };
});
}
flightStatus.current.inFlight = true;
const firstReq = fn(...args);
void (async () => {
try {
await firstReq;
}
finally {
// If it failed, we naively just move on to the next request.
}
while (flightStatus.current.upNext) {
const cur = flightStatus.current.upNext;
flightStatus.current.upNext = null;
await cur
.fn(...cur.args)
.then(cur.resolve)
.catch(cur.reject);
}
flightStatus.current.inFlight = false;
})();
return firstReq;
}, [fn]);
}

View File

@@ -28,9 +28,14 @@
font-style: normal; font-style: normal;
} }
html {
height: 100%;
}
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100%;
font-family: "gg sans", "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: "gg sans", "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
background-color: var(--bg-primary); background-color: var(--bg-primary);
color: var(--text-normal); color: var(--text-normal);
@@ -1220,15 +1225,15 @@ body {
} }
.context-menu-item:hover { .context-menu-item:hover {
background-color: hsl(240 calc(1*4%) 60.784% /0.0784313725490196); background-color: hsla(240, 4%, 60.784%, 0.078);
} }
.context-menu-item-danger { .context-menu-item-danger {
color: color-mix(in oklab, hsl(1.353 calc(1*82.609%) 68.431% /1) 100%, #000 0%); color: hsl(1.353, 82.609%, 68.431%);
} }
.context-menu-item-danger:hover { .context-menu-item-danger:hover {
background-color: color-mix(in oklab,hsl(355.636 calc(1*64.706%) 50% /0.0784313725490196) 100%,hsl(0 0% 0% /0.0784313725490196) 0%); background-color: hsla(355.636, 64.706%, 50%, 0.078);
} }
.context-menu-checkbox-item { .context-menu-checkbox-item {
@@ -1255,8 +1260,8 @@ body {
} }
.context-menu-checkbox-indicator.checked { .context-menu-checkbox-indicator.checked {
background-color: hsl(235 86% 65%); background-color: hsl(235, 86%, 65%);
border-color: hsl(235 86% 65%); border-color: hsl(235, 86%, 65%);
} }
.context-menu-separator { .context-menu-separator {
@@ -1465,6 +1470,7 @@ body {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
margin-top: 8px;
margin-bottom: 8px; margin-bottom: 8px;
} }
@@ -3086,3 +3092,103 @@ body {
outline: 2px dashed var(--brand-experiment); outline: 2px dashed var(--brand-experiment);
border-radius: 4px; border-radius: 4px;
} }
/* ============================================
MOBILE BACK BUTTON
============================================ */
.mobile-back-btn {
background: none;
border: none;
color: var(--header-secondary);
cursor: pointer;
padding: 4px;
margin-right: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.mobile-back-btn:hover {
color: var(--header-primary);
}
/* ============================================
MOBILE RESPONSIVE (max-width: 768px)
============================================ */
@media (max-width: 768px) {
/* App container: full dynamic viewport, no titlebar gap */
.app-container.is-mobile {
height: 100dvh;
padding-bottom: env(safe-area-inset-bottom, 0px);
box-sizing: border-box;
}
/* Sidebar fills entire screen on mobile */
.is-mobile .sidebar {
width: 100vw;
min-width: 100vw;
}
/* Hide members list on mobile (also enforced in JS) */
.is-mobile .members-list {
display: none !important;
}
/* Auth box responsive */
.auth-box {
width: calc(100vw - 32px);
max-width: 480px;
}
/* Pinned panel full-width on mobile */
.is-mobile .pinned-panel {
width: 100vw;
right: 0;
border-radius: 0;
}
/* Toast container centered on mobile */
.is-mobile .toast-container {
right: auto;
left: 50%;
transform: translateX(-50%);
bottom: 16px;
}
.is-mobile .toast {
min-width: 260px;
}
/* Responsive modals */
.create-channel-modal {
width: calc(100vw - 32px);
}
.theme-selector-modal {
width: calc(100vw - 32px);
}
.avatar-crop-dialog {
width: calc(100vw - 32px);
}
.forced-update-modal {
width: calc(100vw - 32px);
}
/* Chat container takes full width */
.is-mobile .chat-container {
width: 100vw;
}
/* Channel topic - hide on very small screens (also hidden via JS) */
.is-mobile .chat-header-topic {
display: none;
}
/* FriendsView takes full width */
.is-mobile .friends-view {
width: 100vw;
}
}

View File

@@ -13,9 +13,11 @@ import { useToasts } from '../components/Toast';
import { PresenceProvider } from '../contexts/PresenceContext'; import { PresenceProvider } from '../contexts/PresenceContext';
import { getUserPref, setUserPref } from '../utils/userPreferences'; import { getUserPref, setUserPref } from '../utils/userPreferences';
import { usePlatform } from '../platform'; import { usePlatform } from '../platform';
import { useIsMobile } from '../hooks/useIsMobile';
const Chat = () => { const Chat = () => {
const { crypto, settings } = usePlatform(); const { crypto, settings } = usePlatform();
const isMobile = useIsMobile();
const [userId, setUserId] = useState(() => localStorage.getItem('userId')); const [userId, setUserId] = useState(() => localStorage.getItem('userId'));
const [username, setUsername] = useState(() => localStorage.getItem('username') || ''); const [username, setUsername] = useState(() => localStorage.getItem('username') || '');
const [view, setView] = useState(() => { const [view, setView] = useState(() => {
@@ -27,6 +29,7 @@ const Chat = () => {
const [activeDMChannel, setActiveDMChannel] = useState(null); const [activeDMChannel, setActiveDMChannel] = useState(null);
const [showMembers, setShowMembers] = useState(true); const [showMembers, setShowMembers] = useState(true);
const [showPinned, setShowPinned] = useState(false); const [showPinned, setShowPinned] = useState(false);
const [mobileView, setMobileView] = useState('sidebar');
const convex = useConvex(); const convex = useConvex();
const { toasts, addToast, removeToast, ToastContainer } = useToasts(); const { toasts, addToast, removeToast, ToastContainer } = useToasts();
@@ -156,15 +159,21 @@ const Chat = () => {
setActiveDMChannel({ channel_id: channelId, other_username: targetUsername }); setActiveDMChannel({ channel_id: channelId, other_username: targetUsername });
setView('me'); setView('me');
if (isMobile) setMobileView('chat');
} catch (err) { } catch (err) {
console.error('Error opening DM:', err); console.error('Error opening DM:', err);
} }
}, [convex]); }, [convex, isMobile]);
const handleSelectChannel = useCallback((channelId) => { const handleSelectChannel = useCallback((channelId) => {
setActiveChannel(channelId); setActiveChannel(channelId);
setShowPinned(false); setShowPinned(false);
if (isMobile) setMobileView('chat');
}, [isMobile]);
const handleMobileBack = useCallback(() => {
setMobileView('sidebar');
}, []); }, []);
const activeChannelObj = channels.find(c => c._id === activeChannel); const activeChannelObj = channels.find(c => c._id === activeChannel);
@@ -173,6 +182,7 @@ const Chat = () => {
const isDMView = view === 'me' && activeDMChannel; const isDMView = view === 'me' && activeDMChannel;
const isServerTextChannel = view === 'server' && activeChannel && activeChannelObj?.type !== 'voice'; const isServerTextChannel = view === 'server' && activeChannel && activeChannelObj?.type !== 'voice';
const currentChannelId = isDMView ? activeDMChannel.channel_id : activeChannel; const currentChannelId = isDMView ? activeDMChannel.channel_id : activeChannel;
const effectiveShowMembers = isMobile ? false : showMembers;
// PiP: show when watching a stream and NOT currently viewing the voice channel in VoiceStage // PiP: show when watching a stream and NOT currently viewing the voice channel in VoiceStage
const isViewingVoiceStage = view === 'server' && activeChannelObj?.type === 'voice' && activeChannel === voiceActiveChannelId; const isViewingVoiceStage = view === 'server' && activeChannelObj?.type === 'voice' && activeChannel === voiceActiveChannelId;
@@ -196,6 +206,8 @@ const Chat = () => {
onToggleMembers={() => {}} onToggleMembers={() => {}}
showMembers={false} showMembers={false}
onTogglePinned={() => setShowPinned(p => !p)} onTogglePinned={() => setShowPinned(p => !p)}
isMobile={isMobile}
onMobileBack={handleMobileBack}
/> />
<div className="chat-content"> <div className="chat-content">
<ChatArea <ChatArea
@@ -215,12 +227,43 @@ const Chat = () => {
</div> </div>
); );
} }
return <FriendsView onOpenDM={openDM} />; return (
<>
{isMobile && (
<div className="chat-header" style={{ position: 'sticky', top: 0, zIndex: 10 }}>
<div className="chat-header-left">
<button className="mobile-back-btn" onClick={handleMobileBack}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<span className="chat-header-name">Friends</span>
</div>
</div>
)}
<FriendsView onOpenDM={openDM} />
</>
);
} }
if (activeChannel) { if (activeChannel) {
if (activeChannelObj?.type === 'voice') { if (activeChannelObj?.type === 'voice') {
return <VoiceStage room={room} channelId={activeChannel} voiceStates={voiceStates} channelName={activeChannelObj?.name} />; return (
<div className="chat-container">
{isMobile && (
<div className="chat-header">
<div className="chat-header-left">
<button className="mobile-back-btn" onClick={handleMobileBack}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<svg width="20" height="20" viewBox="0 0 24 24" style={{ color: 'var(--text-muted)', marginRight: 4 }}>
<path fill="currentColor" d="M11.383 3.07904C11.009 2.92504 10.579 3.01004 10.293 3.29604L6.586 7.00304H3C2.45 7.00304 2 7.45304 2 8.00304V16.003C2 16.553 2.45 17.003 3 17.003H6.586L10.293 20.71C10.579 20.996 11.009 21.082 11.383 20.927C11.757 20.772 12 20.407 12 20.003V4.00304C12 3.59904 11.757 3.23404 11.383 3.07904Z" />
</svg>
<span className="chat-header-name">{activeChannelObj?.name}</span>
</div>
</div>
)}
<VoiceStage room={room} channelId={activeChannel} voiceStates={voiceStates} channelName={activeChannelObj?.name} />
</div>
);
} }
return ( return (
<div className="chat-container"> <div className="chat-container">
@@ -229,9 +272,11 @@ const Chat = () => {
channelType="text" channelType="text"
channelTopic={activeChannelObj?.topic} channelTopic={activeChannelObj?.topic}
onToggleMembers={() => setShowMembers(!showMembers)} onToggleMembers={() => setShowMembers(!showMembers)}
showMembers={showMembers} showMembers={effectiveShowMembers}
onTogglePinned={() => setShowPinned(p => !p)} onTogglePinned={() => setShowPinned(p => !p)}
serverName={serverName} serverName={serverName}
isMobile={isMobile}
onMobileBack={handleMobileBack}
/> />
<div className="chat-content"> <div className="chat-content">
<ChatArea <ChatArea
@@ -241,7 +286,7 @@ const Chat = () => {
channelKey={channelKeys[activeChannel]} channelKey={channelKeys[activeChannel]}
username={username} username={username}
userId={userId} userId={userId}
showMembers={showMembers} showMembers={effectiveShowMembers}
onToggleMembers={() => setShowMembers(!showMembers)} onToggleMembers={() => setShowMembers(!showMembers)}
onOpenDM={openDM} onOpenDM={openDM}
showPinned={showPinned} showPinned={showPinned}
@@ -249,7 +294,7 @@ const Chat = () => {
/> />
<MembersList <MembersList
channelId={activeChannel} channelId={activeChannel}
visible={showMembers} visible={effectiveShowMembers}
onMemberClick={(member) => {}} onMemberClick={(member) => {}}
/> />
</div> </div>
@@ -266,6 +311,16 @@ const Chat = () => {
); );
} }
const handleSetActiveDMChannel = useCallback((dm) => {
setActiveDMChannel(dm);
if (isMobile && dm) setMobileView('chat');
}, [isMobile]);
const handleViewChange = useCallback((newView) => {
setView(newView);
if (isMobile) setMobileView('sidebar');
}, [isMobile]);
if (!userId) { if (!userId) {
return ( return (
<div className="app-container"> <div className="app-container">
@@ -276,9 +331,13 @@ const Chat = () => {
); );
} }
const showSidebar = !isMobile || mobileView === 'sidebar';
const showMainContent = !isMobile || mobileView === 'chat';
return ( return (
<PresenceProvider userId={userId}> <PresenceProvider userId={userId}>
<div className="app-container"> <div className={`app-container${isMobile ? ' is-mobile' : ''}`}>
{showSidebar && (
<Sidebar <Sidebar
channels={channels} channels={channels}
categories={categories} categories={categories}
@@ -287,16 +346,18 @@ const Chat = () => {
username={username} username={username}
channelKeys={channelKeys} channelKeys={channelKeys}
view={view} view={view}
onViewChange={setView} onViewChange={handleViewChange}
onOpenDM={openDM} onOpenDM={openDM}
activeDMChannel={activeDMChannel} activeDMChannel={activeDMChannel}
setActiveDMChannel={setActiveDMChannel} setActiveDMChannel={handleSetActiveDMChannel}
dmChannels={dmChannels} dmChannels={dmChannels}
userId={userId} userId={userId}
serverName={serverName} serverName={serverName}
serverIconUrl={serverIconUrl} serverIconUrl={serverIconUrl}
isMobile={isMobile}
/> />
{renderMainContent()} )}
{showMainContent && renderMainContent()}
{showPiP && <FloatingStreamPiP onGoBackToStream={handleGoBackToStream} />} {showPiP && <FloatingStreamPiP onGoBackToStream={handleGoBackToStream} />}
<ToastContainer /> <ToastContainer />
</div> </div>

View File

@@ -50,7 +50,7 @@
--border-muted: rgba(255, 255, 255, 0.04); --border-muted: rgba(255, 255, 255, 0.04);
--border-normal: rgba(255, 255, 255, 0.2); --border-normal: rgba(255, 255, 255, 0.2);
--border-strong: rgba(255, 255, 255, 0.44); --border-strong: rgba(255, 255, 255, 0.44);
--app-frame-border: color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.12156862745098039) 100%,hsl(0 0% 0% /0.12156862745098039) 0%); --app-frame-border: hsla(240, 4%, 60.784%, 0.122);
/* Icons */ /* Icons */
--icon-default: #dbdee1; --icon-default: #dbdee1;
@@ -95,7 +95,7 @@
--background-modifier-selected: rgba(78, 80, 88, 0.6); --background-modifier-selected: rgba(78, 80, 88, 0.6);
--div-border: #1e1f22; --div-border: #1e1f22;
--text-feedback-warning: color-mix(in oklab, hsl(38.455 calc(1*100%) 43.137% /1) 100%, #000 0%); --text-feedback-warning: hsl(38.455, 100%, 43.137%);
} }
@@ -141,7 +141,7 @@
--border-muted: rgba(0, 0, 0, 0.2); --border-muted: rgba(0, 0, 0, 0.2);
--border-normal: rgba(0, 0, 0, 0.36); --border-normal: rgba(0, 0, 0, 0.36);
--border-strong: rgba(0, 0, 0, 0.48); --border-strong: rgba(0, 0, 0, 0.48);
--app-frame-border: color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.12156862745098039) 100%,hsl(0 0% 0% /0.12156862745098039) 0%); --app-frame-border: hsla(240, 4%, 60.784%, 0.122);
/* Icons */ /* Icons */
--icon-default: #313338; --icon-default: #313338;
@@ -186,7 +186,7 @@
--background-modifier-selected: rgba(116, 124, 138, 0.30); --background-modifier-selected: rgba(116, 124, 138, 0.30);
--div-border: #e1e2e4; --div-border: #e1e2e4;
--text-feedback-warning: color-mix(in oklab, hsl(38.455 calc(1*100%) 43.137% /1) 100%, #000 0%); --text-feedback-warning: hsl(38.455, 100%, 43.137%);
} }
@@ -204,7 +204,7 @@
--chat-background: #202225; --chat-background: #202225;
--channeltextarea-background: #252529; --channeltextarea-background: #252529;
--modal-background: #292b2f; --modal-background: #292b2f;
--panel-bg: color-mix(in oklab, hsl(240 calc(1*5.882%) 13.333% /1) 100%, #000 0%); --panel-bg: hsl(240, 5.882%, 13.333%);
--embed-background: #242529; --embed-background: #242529;
/* Text */ /* Text */
@@ -232,7 +232,7 @@
--border-muted: rgba(255, 255, 255, 0.04); --border-muted: rgba(255, 255, 255, 0.04);
--border-normal: rgba(255, 255, 255, 0.2); --border-normal: rgba(255, 255, 255, 0.2);
--border-strong: rgba(255, 255, 255, 0.44); --border-strong: rgba(255, 255, 255, 0.44);
--app-frame-border: color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.12156862745098039) 100%,hsl(0 0% 0% /0.12156862745098039) 0%); --app-frame-border: hsla(240, 4%, 60.784%, 0.122);
/* Icons */ /* Icons */
--icon-default: #dddfe4; --icon-default: #dddfe4;
@@ -277,7 +277,7 @@
--background-modifier-selected: rgba(78, 80, 88, 0.4); --background-modifier-selected: rgba(78, 80, 88, 0.4);
--div-border: #111214; --div-border: #111214;
--text-feedback-warning: color-mix(in oklab, hsl(38.455 calc(1*100%) 43.137% /1) 100%, #000 0%); --text-feedback-warning: hsl(38.455, 100%, 43.137%);
} }
@@ -323,7 +323,7 @@
--border-muted: rgba(255, 255, 255, 0.16); --border-muted: rgba(255, 255, 255, 0.16);
--border-normal: rgba(255, 255, 255, 0.24); --border-normal: rgba(255, 255, 255, 0.24);
--border-strong: rgba(255, 255, 255, 0.44); --border-strong: rgba(255, 255, 255, 0.44);
--app-frame-border: color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.12156862745098039) 100%,hsl(0 0% 0% /0.12156862745098039) 0%); --app-frame-border: hsla(240, 4%, 60.784%, 0.122);
/* Icons */ /* Icons */
--icon-default: #e0def0; --icon-default: #e0def0;
@@ -368,5 +368,5 @@
--background-modifier-selected: rgba(78, 73, 106, 0.48); --background-modifier-selected: rgba(78, 73, 106, 0.48);
--div-border: #080810; --div-border: #080810;
--text-feedback-warning: color-mix(in oklab, hsl(38.455 calc(1*100%) 43.137% /1) 100%, #000 0%); --text-feedback-warning: hsl(38.455, 100%, 43.137%);
} }