diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c040e19..ff3181e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -33,7 +33,8 @@ "Bash(npm ls:*)", "Bash(git add:*)", "Bash(git commit:*)", - "Bash(git push:*)" + "Bash(git push:*)", + "Bash(npm run build:web:*)" ] } } diff --git a/TODO.md b/TODO.md index 5afcb34..9348ae4 100644 --- a/TODO.md +++ b/TODO.md @@ -32,14 +32,25 @@ - - -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. - +- 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. \ No newline at end of file + + +- 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. + + + + + + + + + +- 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. \ No newline at end of file diff --git a/apps/electron/package.json b/apps/electron/package.json index 03bfd27..64f0fa8 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -1,7 +1,7 @@ { "name": "@discord-clone/electron", "private": true, - "version": "1.0.14", + "version": "1.0.16", "description": "Discord Clone - Electron app", "author": "Moyettes", "type": "module", diff --git a/apps/web/index.html b/apps/web/index.html index 11b22e4..c38c18c 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -3,7 +3,7 @@ - + Discord Clone diff --git a/convex/auth.js b/convex/auth.js new file mode 100644 index 0000000..5d99e4a --- /dev/null +++ b/convex/auth.js @@ -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]; + } + }); + }); }, +}); diff --git a/convex/categories.js b/convex/categories.js new file mode 100644 index 0000000..89be86e --- /dev/null +++ b/convex/categories.js @@ -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]; + } + }); + }); }, +}); diff --git a/convex/channelKeys.js b/convex/channelKeys.js new file mode 100644 index 0000000..6eecd06 --- /dev/null +++ b/convex/channelKeys.js @@ -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, + }); })]; + } + }); + }); }, +}); diff --git a/convex/channels.js b/convex/channels.js new file mode 100644 index 0000000..3dcd7c3 --- /dev/null +++ b/convex/channels.js @@ -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 }]; + } + }); + }); }, +}); diff --git a/convex/convex.config.js b/convex/convex.config.js new file mode 100644 index 0000000..582edf3 --- /dev/null +++ b/convex/convex.config.js @@ -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; diff --git a/convex/dms.js b/convex/dms.js new file mode 100644 index 0000000..419abd7 --- /dev/null +++ b/convex/dms.js @@ -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; })]; + } + }); + }); }, +}); diff --git a/convex/files.js b/convex/files.js new file mode 100644 index 0000000..0363018 --- /dev/null +++ b/convex/files.js @@ -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()]; + } + }); + }); }, +}); diff --git a/convex/gifs.js b/convex/gifs.js new file mode 100644 index 0000000..b463846 --- /dev/null +++ b/convex/gifs.js @@ -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: [] }]; + }); + }); }, +}); diff --git a/convex/invites.js b/convex/invites.js new file mode 100644 index 0000000..ed79e50 --- /dev/null +++ b/convex/invites.js @@ -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 }]; + } + }); + }); }, +}); diff --git a/convex/members.js b/convex/members.js new file mode 100644 index 0000000..446281a --- /dev/null +++ b/convex/members.js @@ -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]; + } + }); + }); }, +}); diff --git a/convex/messages.js b/convex/messages.js new file mode 100644 index 0000000..505627f --- /dev/null +++ b/convex/messages.js @@ -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]; + } + }); + }); }, +}); diff --git a/convex/presence.js b/convex/presence.js new file mode 100644 index 0000000..81ccbc5 --- /dev/null +++ b/convex/presence.js @@ -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()]; + } + }); + }); }, +}); diff --git a/convex/reactions.js b/convex/reactions.js new file mode 100644 index 0000000..5d8fbed --- /dev/null +++ b/convex/reactions.js @@ -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]; + } + }); + }); }, +}); diff --git a/convex/readState.js b/convex/readState.js new file mode 100644 index 0000000..e10ee74 --- /dev/null +++ b/convex/readState.js @@ -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]; + } + }); + }); }, +}); diff --git a/convex/roles.js b/convex/roles.js new file mode 100644 index 0000000..7c1022c --- /dev/null +++ b/convex/roles.js @@ -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]; + } + }); + }); }, +}); diff --git a/convex/schema.js b/convex/schema.js new file mode 100644 index 0000000..8d68aeb --- /dev/null +++ b/convex/schema.js @@ -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")), + }), +}); diff --git a/convex/serverSettings.js b/convex/serverSettings.js new file mode 100644 index 0000000..4c8fb60 --- /dev/null +++ b/convex/serverSettings.js @@ -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]; + } + }); + }); }, +}); diff --git a/convex/storageUrl.js b/convex/storageUrl.js new file mode 100644 index 0000000..1882ae3 --- /dev/null +++ b/convex/storageUrl.js @@ -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)]; + } + }); + }); +} diff --git a/convex/typing.js b/convex/typing.js new file mode 100644 index 0000000..cc51361 --- /dev/null +++ b/convex/typing.js @@ -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]; + } + }); + }); }, +}); diff --git a/convex/voice.js b/convex/voice.js new file mode 100644 index 0000000..9e661ae --- /dev/null +++ b/convex/voice.js @@ -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 }]; + } + }); + }); }, +}); diff --git a/convex/voiceState.js b/convex/voiceState.js new file mode 100644 index 0000000..33387f2 --- /dev/null +++ b/convex/voiceState.js @@ -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]; + } + }); + }); }, +}); diff --git a/convex/voiceState.ts b/convex/voiceState.ts index 49fbc99..18296a2 100644 --- a/convex/voiceState.ts +++ b/convex/voiceState.ts @@ -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)?.["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({ args: { actorUserId: v.id("userProfiles"), diff --git a/log/log1.txt b/log/log1.txt deleted file mode 100644 index 0170dcf..0000000 --- a/log/log1.txt +++ /dev/null @@ -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: ArgsEscaped:false Image: Volumes:map[] WorkingDir: Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout: 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: ArgsEscaped:false Image:moyettes/eb Volumes:map[] WorkingDir:/workspace/Moyettes/DiscordClone Entrypoint:[/bin/sleep 10800] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout: 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:} -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: VolumeOptions: TmpfsOptions: ClusterOptions:} {Type:volume Source:GITEA-ACTIONS-TASK-41_WORKFLOW-Build-and-Release_JOB-build-and-release-env Target:/var/run/act ReadOnly:false Consistency: BindOptions: VolumeOptions: TmpfsOptions: ClusterOptions:} {Type:volume Source:GITEA-ACTIONS-TASK-41_WORKFLOW-Build-and-Release_JOB-build-and-release Target:/workspace/Moyettes/DiscordClone ReadOnly:false Consistency: BindOptions: VolumeOptions: TmpfsOptions: ClusterOptions:}] MaskedPaths:[] ReadonlyPaths:[] Init:} -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 -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 -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: ) (original error: ) diff --git a/log/log2.txt b/log/log2.txt deleted file mode 100644 index fce55ff..0000000 --- a/log/log2.txt +++ /dev/null @@ -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: ArgsEscaped:false Image: Volumes:map[] WorkingDir: Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout: 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: ArgsEscaped:false Image:moyettes/eb Volumes:map[] WorkingDir:/workspace/Moyettes/DiscordClone Entrypoint:[/bin/sleep 10800] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout: 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:} -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: VolumeOptions: TmpfsOptions: ClusterOptions:} {Type:volume Source:GITEA-ACTIONS-TASK-44_WORKFLOW-Build-and-Release_JOB-build-and-release-env Target:/var/run/act ReadOnly:false Consistency: BindOptions: VolumeOptions: TmpfsOptions: ClusterOptions:} {Type:volume Source:GITEA-ACTIONS-TASK-44_WORKFLOW-Build-and-Release_JOB-build-and-release Target:/workspace/Moyettes/DiscordClone ReadOnly:false Consistency: BindOptions: VolumeOptions: TmpfsOptions: ClusterOptions:}] MaskedPaths:[] ReadonlyPaths:[] Init:} -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 -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 -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 diff --git a/log/log3.txt b/log/log3.txt deleted file mode 100644 index 478781a..0000000 --- a/log/log3.txt +++ /dev/null @@ -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: ArgsEscaped:false Image: Volumes:map[] WorkingDir: Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout: 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: ArgsEscaped:false Image:moyettes/eb Volumes:map[] WorkingDir:/workspace/Moyettes/DiscordClone Entrypoint:[/bin/sleep 10800] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout: 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:} -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: VolumeOptions: TmpfsOptions: ClusterOptions:} {Type:volume Source:GITEA-ACTIONS-TASK-45_WORKFLOW-Build-and-Release_JOB-build-and-release-env Target:/var/run/act ReadOnly:false Consistency: BindOptions: VolumeOptions: TmpfsOptions: ClusterOptions:} {Type:volume Source:GITEA-ACTIONS-TASK-45_WORKFLOW-Build-and-Release_JOB-build-and-release Target:/workspace/Moyettes/DiscordClone ReadOnly:false Consistency: BindOptions: VolumeOptions: TmpfsOptions: ClusterOptions:}] MaskedPaths:[] ReadonlyPaths:[] Init:} -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 -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 -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 diff --git a/log/log4.txt b/log/log4.txt deleted file mode 100644 index 298924f..0000000 --- a/log/log4.txt +++ /dev/null @@ -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: ArgsEscaped:false Image: Volumes:map[] WorkingDir: Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout: 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: ArgsEscaped:false Image:moyettes/eb Volumes:map[] WorkingDir:/workspace/Moyettes/DiscordClone Entrypoint:[/bin/sleep 10800] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout: 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:} -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: VolumeOptions: TmpfsOptions: ClusterOptions:} {Type:volume Source:GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release-env Target:/var/run/act ReadOnly:false Consistency: BindOptions: VolumeOptions: TmpfsOptions: ClusterOptions:} {Type:volume Source:GITEA-ACTIONS-TASK-46_WORKFLOW-Build-and-Release_JOB-build-and-release Target:/workspace/Moyettes/DiscordClone ReadOnly:false Consistency: BindOptions: VolumeOptions: TmpfsOptions: ClusterOptions:}] MaskedPaths:[] ReadonlyPaths:[] Init:} -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 -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 -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. (/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 diff --git a/packages/platform-web/src/crypto.js b/packages/platform-web/src/crypto.js index d179247..4ee0878 100644 --- a/packages/platform-web/src/crypto.js +++ b/packages/platform-web/src/crypto.js @@ -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 --- 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 }) => { try { const verified = await verifySignature(publicKey, message, signature); diff --git a/packages/shared/package.json b/packages/shared/package.json index f5b5809..45d8ae4 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,7 +1,7 @@ { "name": "@discord-clone/shared", "private": true, - "version": "1.0.14", + "version": "1.0.16", "type": "module", "main": "src/App.jsx", "dependencies": { diff --git a/packages/shared/src/App.jsx b/packages/shared/src/App.jsx index 0d58ef4..5c748d0 100644 --- a/packages/shared/src/App.jsx +++ b/packages/shared/src/App.jsx @@ -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 Login from './pages/Login'; import Register from './pages/Register'; @@ -62,21 +62,18 @@ function AuthGuard({ children }) { return () => { cancelled = true; }; }, []); - // Redirect once after auth state is determined (not on every route change) - const hasRedirected = useRef(false); - useEffect(() => { - if (authState === 'loading' || hasRedirected.current) return; - hasRedirected.current = true; + if (authState === 'loading') return; 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 }); - } else if (authState === 'unauthenticated' && !isAuthPage) { + } else if (!hasSession && !isAuthPage) { navigate('/', { replace: true }); } - }, [authState]); + }, [authState, location.pathname]); if (authState === 'loading') { return ( diff --git a/packages/shared/src/components/ChatArea.jsx b/packages/shared/src/components/ChatArea.jsx index e726682..0370fc8 100644 --- a/packages/shared/src/components/ChatArea.jsx +++ b/packages/shared/src/components/ChatArea.jsx @@ -24,7 +24,9 @@ import UserProfilePopup from './UserProfilePopup'; import Avatar from './Avatar'; import MentionMenu from './MentionMenu'; import MessageItem, { getUserColor } from './MessageItem'; +import ColoredIcon from './ColoredIcon'; import { usePlatform } from '../platform'; +import { useVoice } from '../contexts/VoiceContext'; const metadataCache = new Map(); const attachmentCache = new Map(); @@ -59,8 +61,8 @@ export function clearMessageDecryptionCache() { messageDecryptionCache.clear(); } -const ICON_COLOR_DEFAULT = 'color-mix(in oklab, hsl(240 4.294% 68.039% / 1) 100%, #000 0%)'; -const ICON_COLOR_DANGER = 'color-mix(in oklab, hsl(1.353 calc(1*82.609%) 68.431% /1) 100%, #000 0%)'; +const ICON_COLOR_DEFAULT = 'hsl(240, 4.294%, 68.039%)'; +const ICON_COLOR_DANGER = 'hsl(1.353, 82.609%, 68.431%)'; const fromHexString = (hexString) => 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 }) => { + const menuRef = useRef(null); + 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 ( +
e.stopPropagation()}> +
{ e.stopPropagation(); onPaste(); onClose(); }}> + Paste +
+
+ ); +}; const ChatArea = ({ channelId, channelName, channelType, username, channelKey, userId: currentUserId, showMembers, onToggleMembers, onOpenDM, showPinned, onTogglePinned }) => { const { crypto } = usePlatform(); + const { isReceivingScreenShareAudio } = useVoice(); const [decryptedMessages, setDecryptedMessages] = useState([]); const [input, setInput] = useState(''); const [zoomedImage, setZoomedImage] = useState(null); @@ -481,6 +502,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u const [isMultiline, setIsMultiline] = useState(false); const [hoveredMessageId, setHoveredMessageId] = useState(null); const [contextMenu, setContextMenu] = useState(null); + const [inputContextMenu, setInputContextMenu] = useState(null); const [uploading, setUploading] = useState(false); const [replyingTo, setReplyingTo] = useState(null); const [editingMessage, setEditingMessage] = useState(null); @@ -563,7 +585,7 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u return { ...msg, content: cached?.content ?? '[Decrypting...]', - isVerified: cached?.isVerified ?? false, + isVerified: cached?.isVerified ?? null, decryptedReply: cached?.decryptedReply ?? null, }; }); @@ -667,14 +689,15 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u const verifyMap = new Map(); for (let i = 0; i < verifyResults.length; 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 for (const msg of needsDecryption) { const content = decryptedMap.get(msg.id) ?? (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; messageDecryptionCache.set(msg.id, { content, isVerified, decryptedReply }); } @@ -715,13 +738,14 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u }, [username, myRoleNames]); const playPingSound = useCallback(() => { + if (isReceivingScreenShareAudio) return; const now = Date.now(); if (now - lastPingTimeRef.current < 1000) return; lastPingTimeRef.current = now; const audio = new Audio(PingSound); audio.volume = 0.5; audio.play().catch(() => {}); - }, []); + }, [isReceivingScreenShareAudio]); // Play ping sound when a new message mentions us (by username or role) useEffect(() => { @@ -1341,6 +1365,38 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u {contextMenu && setContextMenu(null)} onInteract={(action) => handleContextInteract(action, contextMenu.messageId)} />} + {inputContextMenu && 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 {} + }} />}
{mentionQuery !== null && mentionItems.length > 0 && ( @@ -1382,6 +1438,10 @@ const ChatArea = ({ channelId, channelName, channelType, username, channelKey, u onBlur={saveSelection} onMouseUp={saveSelection} onKeyUp={saveSelection} + onContextMenu={(e) => { + e.preventDefault(); + setInputContextMenu({ x: e.clientX, y: e.clientY }); + }} onPaste={(e) => { const items = e.clipboardData?.items; if (items) { diff --git a/packages/shared/src/components/ChatHeader.jsx b/packages/shared/src/components/ChatHeader.jsx index 9146f6e..ae5edbf 100644 --- a/packages/shared/src/components/ChatHeader.jsx +++ b/packages/shared/src/components/ChatHeader.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; 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 isDM = channelType === 'dm'; @@ -10,9 +10,14 @@ const ChatHeader = ({ channelName, channelType, channelTopic, onToggleMembers, s return (
+ {isMobile && onMobileBack && ( + + )} {isDM ? '@' : '#'} {channelName} - {channelTopic && !isDM && ( + {channelTopic && !isDM && !isMobile && ( <>
{channelTopic} @@ -21,7 +26,7 @@ const ChatHeader = ({ channelName, channelType, channelTopic, onToggleMembers, s {isDM && }
- {!isDM && ( + {!isDM && !isMobile && ( - {!isDM && ( + {!isDM && !isMobile && ( - -
- setSearchFocused(true)} - onBlur={() => setSearchFocused(false)} - /> -
+ {!isMobile && ( + + + + )} + {!isMobile && ( +
+ setSearchFocused(true)} + onBlur={() => setSearchFocused(false)} + /> +
+ )}
); diff --git a/packages/shared/src/components/ColoredIcon.jsx b/packages/shared/src/components/ColoredIcon.jsx new file mode 100644 index 0000000..6487e07 --- /dev/null +++ b/packages/shared/src/components/ColoredIcon.jsx @@ -0,0 +1,31 @@ +import React from 'react'; + +const ColoredIcon = React.memo(({ src, color, size = '20px', style = {} }) => { + if (!color) { + return ( +
+ +
+ ); + } + return ( +
+ +
+ ); +}); + +export default ColoredIcon; diff --git a/packages/shared/src/components/DMList.jsx b/packages/shared/src/components/DMList.jsx index a687e6a..381cebd 100644 --- a/packages/shared/src/components/DMList.jsx +++ b/packages/shared/src/components/DMList.jsx @@ -3,6 +3,7 @@ import { useConvex } from 'convex/react'; import { api } from '../../../../convex/_generated/api'; import Tooltip from './Tooltip'; import Avatar from './Avatar'; +import ColoredIcon from './ColoredIcon'; import { useOnlineUsers } from '../contexts/PresenceContext'; import friendsIcon from '../assets/icons/friends.svg'; @@ -181,26 +182,7 @@ const DMList = ({ dmChannels, activeDMChannel, onSelectDM, onOpenDM }) => { onClick={() => onSelectDM('friends')} >
-
- -
+
Friends
diff --git a/packages/shared/src/components/FriendsView.jsx b/packages/shared/src/components/FriendsView.jsx index 8a53e36..c0008f6 100644 --- a/packages/shared/src/components/FriendsView.jsx +++ b/packages/shared/src/components/FriendsView.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { useQuery } from 'convex/react'; import { api } from '../../../../convex/_generated/api'; import Avatar from './Avatar'; +import ColoredIcon from './ColoredIcon'; import { useOnlineUsers } from '../contexts/PresenceContext'; import friendsIcon from '../assets/icons/friends.svg'; @@ -54,26 +55,7 @@ const FriendsView = ({ onOpenDM }) => { }}>
-
- -
+
Friends
diff --git a/packages/shared/src/components/MembersList.jsx b/packages/shared/src/components/MembersList.jsx index a826261..ff9f0b8 100644 --- a/packages/shared/src/components/MembersList.jsx +++ b/packages/shared/src/components/MembersList.jsx @@ -4,6 +4,7 @@ import { api } from '../../../../convex/_generated/api'; import { useOnlineUsers } from '../contexts/PresenceContext'; import { useVoice } from '../contexts/VoiceContext'; import { CrownIcon, SharingIcon } from '../assets/icons'; +import ColoredIcon from './ColoredIcon'; 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]; } -const ColoredIcon = ({ src, color, size = '24px', style = {} }) => ( -
- -
-); - const STATUS_COLORS = { online: '#3ba55c', idle: '#faa61a', diff --git a/packages/shared/src/components/MessageItem.jsx b/packages/shared/src/components/MessageItem.jsx index 40bc12a..f003292 100644 --- a/packages/shared/src/components/MessageItem.jsx +++ b/packages/shared/src/components/MessageItem.jsx @@ -14,13 +14,14 @@ import { import { getEmojiUrl, AllEmojis } from '../assets/emojis'; import Tooltip from './Tooltip'; import Avatar from './Avatar'; +import ColoredIcon from './ColoredIcon'; import { usePlatform } from '../platform'; const fireIcon = getEmojiUrl('nature', 'fire'); const heartIcon = getEmojiUrl('symbols', 'heart'); 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']; export const getUserColor = (name) => { @@ -99,12 +100,6 @@ const getReactionIcon = (name) => { } }; -const ColoredIcon = ({ src, color, size = '24px', style = {} }) => ( -
- -
-); - const isNewDay = (current, previous) => { if (!previous) return true; return current.getDate() !== previous.getDate() @@ -123,7 +118,7 @@ const createMarkdownComponents = (openExternal) => ({ return {props.children}; } } - if (props.href && props.href.startsWith('mention://')) return {props.children}; + if (props.href && props.href.startsWith('mention://')) return {props.children}; return { 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 }) { @@ -163,7 +158,7 @@ const MessageToolbar = ({ onAddReaction, onEdit, onReply, onMore, isOwner }) => onAddReaction('fire')} emoji={} /> -
+
onAddReaction(null)} emoji={} /> @@ -262,7 +257,7 @@ const MessageItem = React.memo(({ return (
{Object.entries(msg.reactions).map(([emojiName, data]) => ( -
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' }}> +
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' }}> {data.count}
@@ -287,12 +282,12 @@ const MessageItem = React.memo(({
- {isMentioned &&
} + {isMentioned &&
} {msg.replyToId && msg.replyToUsername && (
onScrollToMessage(msg.replyToId)}> @@ -330,7 +325,7 @@ const MessageItem = React.memo(({ > {msg.username || 'Unknown'} - {!msg.isVerified && } + {msg.isVerified === false && } {currentDate.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
)} diff --git a/packages/shared/src/components/Sidebar.jsx b/packages/shared/src/components/Sidebar.jsx index 8556e98..cf6b31b 100644 --- a/packages/shared/src/components/Sidebar.jsx +++ b/packages/shared/src/components/Sidebar.jsx @@ -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 { useConvex, useMutation, useQuery } from 'convex/react'; 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 { getUserPref, setUserPref } from '../utils/userPreferences'; import { usePlatform } from '../platform'; +import ColoredIcon from './ColoredIcon'; 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_ACTIVE = 'hsl(357.692 67.826% 54.902% / 1)'; -const SERVER_MUTE_RED = 'color-mix(in oklab, hsl(1.343 calc(1*84.81%) 69.02% /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%)'; +const SERVER_MUTE_RED = 'hsl(1.343, 84.81%, 69.02%)'; const controlButtonStyle = { background: 'transparent', @@ -68,29 +69,6 @@ function randomHex(length) { return bytesToHex(bytes); } -const ColoredIcon = ({ src, color, size = '20px' }) => ( -
- -
-); - const VoiceTimer = () => { const [elapsed, setElapsed] = React.useState(0); React.useEffect(() => { @@ -114,7 +92,7 @@ const STATUS_OPTIONS = [ { value: 'invisible', label: 'Invisible', color: '#747f8d' }, ]; -const UserControlPanel = ({ username, userId }) => { +const UserControlPanel = React.memo(({ username, userId }) => { const { session, idle } = usePlatform(); const { isMuted, isDeafened, toggleMute, toggleDeafen, connectionState, disconnectVoice } = useVoice(); const [showStatusMenu, setShowStatusMenu] = useState(false); @@ -124,23 +102,32 @@ const UserControlPanel = ({ username, userId }) => { const navigate = useNavigate(); const manualStatusRef = useRef(false); 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 const allUsers = useQuery(api.auth.getPublicKeys) || []; const myUser = allUsers.find(u => u.id === userId); React.useEffect(() => { if (myUser) { - if (myUser.status && myUser.status !== 'offline') { - setCurrentStatus(myUser.status); - // dnd/invisible are manual overrides; idle is auto-set so don't count it - manualStatusRef.current = (myUser.status === 'dnd' || myUser.status === 'invisible'); - } else if (!myUser.status || myUser.status === 'offline') { - // First login or no preference set yet — default to "online" + const isInitial = !hasInitializedRef.current; + if (isInitial) hasInitializedRef.current = true; + + // 'idle' is auto-set by the idle detector, not a user preference — + // on a fresh app launch, reset it to 'online' just like 'offline' + const shouldReset = !myUser.status || myUser.status === 'offline' + || (isInitial && myUser.status === 'idle'); + + if (shouldReset) { setCurrentStatus('online'); manualStatusRef.current = false; if (userId) { updateStatusMutation({ userId, status: 'online' }).catch(() => {}); } + } else if (myUser.status) { + setCurrentStatus(myUser.status); + manualStatusRef.current = (myUser.status === 'dnd' || myUser.status === 'invisible'); } } }, [myUser?.status]); @@ -188,13 +175,13 @@ const UserControlPanel = ({ username, userId }) => { } }; - // Auto-idle detection via Electron powerMonitor + // Auto-idle detection via platform idle API useEffect(() => { if (!idle || !userId) return; const handleIdleChange = (data) => { if (manualStatusRef.current) return; if (data.isIdle) { - preIdleStatusRef.current = currentStatus; + preIdleStatusRef.current = currentStatusRef.current; setCurrentStatus('idle'); updateStatusMutation({ userId, status: 'idle' }).catch(() => {}); } else { @@ -294,7 +281,7 @@ const UserControlPanel = ({ username, userId }) => { )}
); -}; +}); @@ -311,9 +298,9 @@ const voicePanelButtonStyle = { flex: 1, alignItems: 'center', minHeight: '32px', - background: 'hsl(240 calc(1*4%) 60.784% /0.0784313725490196)', - border: 'hsl(0 calc(1*0%) 100% /0.0784313725490196)', - borderColor: 'color-mix(in oklab,hsl(240 calc(1*4%) 60.784% /0.0392156862745098) 100%,hsl(0 0% 0% /0.0392156862745098) 0%)', + background: 'hsla(240, 4%, 60.784%, 0.078)', + border: 'hsla(0, 0%, 100%, 0.078)', + borderColor: 'hsla(240, 4%, 60.784%, 0.039)', borderRadius: '8px', cursor: 'pointer', padding: '4px', @@ -332,7 +319,7 @@ const liveBadgeStyle = { height: '16px', minHeight: '16px', minWidth: '16px', - color: 'hsl(0 calc(1*0%) 100% /1)', + color: 'hsl(0, 0%, 100%)', fontSize: '12px', fontWeight: '700', letterSpacing: '.02em', @@ -343,8 +330,8 @@ const liveBadgeStyle = { 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 VOICE_ACTIVE_COLOR = "color-mix(in oklab, hsl(132.809 calc(1*34.902%) 50% /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 = 'hsl(132.809, 34.902%, 50%)'; async function encryptKeyForUsers(convex, channelId, keyHex, crypto) { 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 [pos, setPos] = useState({ top: y, left: x }); @@ -428,7 +415,7 @@ const VoiceUserContextMenu = ({ x, y, onClose, user, onMute, isMuted, onServerMu value={userVolume} onChange={(e) => onVolumeChange(Number(e.target.value))} 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}%)` }} />
@@ -479,6 +466,14 @@ const VoiceUserContextMenu = ({ x, y, onClose, user, onMute, isMuted, onServerMu
)} + {!isSelf && hasDisconnectPermission && ( +
{ e.stopPropagation(); onDisconnect(); onClose(); }} + > + Disconnect +
+ )}
{ e.stopPropagation(); onMessage(); onClose(); }}> Message @@ -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 [isCreating, setIsCreating] = useState(false); const [isServerSettingsOpen, setIsServerSettingsOpen] = useState(false); @@ -829,6 +824,8 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam [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); useEffect(() => { @@ -843,15 +840,17 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam for (const id of currentIds) { if (!prevUnreadDMsRef.current.has(id)) { - const audio = new Audio(PingSound); - audio.volume = 0.5; - audio.play().catch(() => {}); + if (!isReceivingScreenShareAudio) { + const audio = new Audio(PingSound); + audio.volume = 0.5; + audio.play().catch(() => {}); + } break; } } prevUnreadDMsRef.current = currentIds; - }, [dmChannels, unreadChannels]); + }, [dmChannels, unreadChannels, isReceivingScreenShareAudio]); const onRenameChannel = () => {}; @@ -859,8 +858,6 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam 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 = () => { setIsCreating(true); setNewChannelName(''); @@ -987,7 +984,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam }); } - new Audio(screenShareStartSound).play(); + if (!isReceivingScreenShareAudio) new Audio(screenShareStartSound).play(); setScreenSharing(true); track.onended = () => { @@ -1017,7 +1014,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam } } room.localParticipant.setScreenShareEnabled(false); - new Audio(screenShareStopSound).play(); + if (!isReceivingScreenShareAudio) new Audio(screenShareStopSound).play(); setScreenSharing(false); } else { setIsScreenShareModalOpen(true); @@ -1025,8 +1022,11 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam }; const handleChannelClick = (channel) => { - if (channel.type === 'voice' && voiceChannelId !== channel._id) { - connectToVoice(channel._id, channel.name, localStorage.getItem('userId')); + if (channel.type === 'voice') { + if (voiceChannelId !== channel._id) { + connectToVoice(channel._id, channel.name, localStorage.getItem('userId')); + } + onSelectChannel(channel._id); } else { onSelectChannel(channel._id); } @@ -1127,11 +1127,18 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam ); }; - const toggleCategory = (cat) => { - const next = { ...collapsedCategories, [cat]: !collapsedCategories[cat] }; - setCollapsedCategories(next); - setUserPref(userId, 'collapsedCategories', next, settings); - }; + const toggleCategory = useCallback((cat) => { + setCollapsedCategories(prev => { + const next = { ...prev, [cat]: !prev[cat] }; + setUserPref(userId, 'collapsedCategories', next, settings); + return next; + }); + }, [userId, settings]); + + const handleAddChannelToCategory = useCallback((groupId) => { + setCreateChannelCategoryId(groupId === '__uncategorized__' ? null : groupId); + setShowCreateChannelModal(true); + }, []); // Group channels by categoryId const groupedChannels = React.useMemo(() => { @@ -1377,12 +1384,10 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam toggleCategory(group.id)} - onAddChannel={() => { - setCreateChannelCategoryId(group.id === '__uncategorized__' ? null : group.id); - setShowCreateChannelModal(true); - }} + onToggle={toggleCategory} + onAddChannel={handleAddChannelToCategory} /> {(() => { const isCollapsed = collapsedCategories[group.id]; @@ -1578,7 +1583,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam margin: '8px 8px 0px 8px', display: 'flex', 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)' }}>
Voice Connected
@@ -1648,6 +1653,8 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam isServerMuted={isServerMuted(voiceUserMenu.user.userId)} onServerMute={() => serverMute(voiceUserMenu.user.userId, !isServerMuted(voiceUserMenu.user.userId))} hasPermission={!!myPermissions.mute_members} + onDisconnect={() => disconnectUser(voiceUserMenu.user.userId)} + hasDisconnectPermission={!!myPermissions.move_members} onMessage={() => { onOpenDM(voiceUserMenu.user.userId, voiceUserMenu.user.username); onViewChange('me'); @@ -1692,16 +1699,16 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam }; // 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 }) => ( +
onToggle(groupId)} {...(dragListeners || {})}> {group.name}
-
-); +)); export default Sidebar; diff --git a/packages/shared/src/components/UpdateBanner.jsx b/packages/shared/src/components/UpdateBanner.jsx index 570920c..84ad847 100644 --- a/packages/shared/src/components/UpdateBanner.jsx +++ b/packages/shared/src/components/UpdateBanner.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, createContext, useContext } from 'react'; import { usePlatform } from '../platform'; +import ColoredIcon from './ColoredIcon'; import updateIcon from '../assets/icons/update.svg'; 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)' }} >
-
- -
+
); diff --git a/packages/shared/src/components/VoiceStage.jsx b/packages/shared/src/components/VoiceStage.jsx index e8eb547..0232ca5 100644 --- a/packages/shared/src/components/VoiceStage.jsx +++ b/packages/shared/src/components/VoiceStage.jsx @@ -15,8 +15,9 @@ import personalMuteIcon from '../assets/icons/personal_mute.svg'; import serverMuteIcon from '../assets/icons/server_mute.svg'; import screenShareStartSound from '../assets/sounds/screenshare_start.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(); @@ -43,30 +44,6 @@ const WATCH_STREAM_BUTTON_STYLE = { const THUMBNAIL_SIZE = { width: 120, height: 68 }; const BOTTOM_BAR_HEIGHT = 140; -// Helper Component for coloring SVGs (Reused from Sidebar) -const ColoredIcon = ({ src, color, size = '24px' }) => ( -
- -
-); - // --- Components --- 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 ( + + + + + + ); + } + if (volume < 50) { + return ( + + + + + ); + } + return ( + + + + + + ); +}; + +// Inline SVG icons for fullscreen +const ExpandIcon = () => ( + + + +); + +const CompressIcon = () => ( + + + +); + const FocusedStreamView = ({ streamParticipant, streamerUsername, @@ -278,11 +296,34 @@ const FocusedStreamView = ({ streamingIdentities, voiceUsers, isTabVisible, + isFullscreen, + onToggleFullscreen, + localIdentity, }) => { const screenTrack = useParticipantTrack(streamParticipant, 'screenshare'); const [barHover, setBarHover] = 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 useEffect(() => { if (!streamParticipant) { @@ -362,23 +403,90 @@ const FocusedStreamView = ({ LIVE
- {/* Top-right: close button */} - + {/* Top-right: button group (fullscreen + close) */} +
+ + +
+ + {/* Bottom-left: volume control (hidden when watching own stream) */} + {!isSelf && ( +
+ + {volumeExpanded && ( + <> + 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}%)`, + }} + /> + + {isMutedByMe ? 0 : userVolume}% + + + )} +
+ )}
{/* Bottom participants bar */} @@ -473,13 +581,37 @@ const FocusedStreamView = ({ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => { 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 [isScreenShareActive, setIsScreenShareActive] = useState(false); const screenShareAudioTrackRef = useRef(null); 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(() => { if (!room) return; @@ -496,7 +628,7 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => { room.on(RoomEvent.ParticipantDisconnected, updateParticipants); room.localParticipant.on('localTrackPublished', updateParticipants); 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(); } updateParticipants(); @@ -510,10 +642,11 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => { }; }, [room]); - // Reset collapsed state when room disconnects + // Reset collapsed state and exit fullscreen when room disconnects useEffect(() => { if (!room) { setParticipantsCollapsed(false); + if (document.fullscreenElement) document.exitFullscreen().catch(console.error); } }, [room]); @@ -524,6 +657,7 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => { ); const handleStopWatching = useCallback(() => { + if (document.fullscreenElement) document.exitFullscreen().catch(console.error); setWatchingStreamOf(null); setParticipantsCollapsed(false); }, []); @@ -589,7 +723,7 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => { } const track = stream.getVideoTracks()[0]; if (track) { - new Audio(screenShareStartSound).play(); + if (!isReceivingScreenShareAudio) new Audio(screenShareStartSound).play(); await room.localParticipant.publishTrack(track, { name: 'screen_share', source: Track.Source.ScreenShare @@ -657,6 +791,21 @@ const VoiceStage = ({ room, channelId, voiceStates, channelName }) => { 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) { return (
{ : null; return ( -
+
{watchingStreamOf && watchedParticipant ? ( /* Focused/Fullscreen View */ { streamingIdentities={streamingIdentities} voiceUsers={voiceUsers} isTabVisible={isTabVisible} + isFullscreen={isFullscreen} + onToggleFullscreen={toggleFullscreen} + localIdentity={room.localParticipant.identity} /> ) : ( /* Grid View */ diff --git a/packages/shared/src/contexts/PresenceContext.jsx b/packages/shared/src/contexts/PresenceContext.jsx index 486fe14..83322e3 100644 --- a/packages/shared/src/contexts/PresenceContext.jsx +++ b/packages/shared/src/contexts/PresenceContext.jsx @@ -1,5 +1,5 @@ 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'; const PresenceContext = createContext({ diff --git a/packages/shared/src/contexts/VoiceContext.jsx b/packages/shared/src/contexts/VoiceContext.jsx index fd139f5..4c3e557 100644 --- a/packages/shared/src/contexts/VoiceContext.jsx +++ b/packages/shared/src/contexts/VoiceContext.jsx @@ -31,7 +31,10 @@ const VoiceContext = createContext(); export const useVoice = () => useContext(VoiceContext); +let _suppressAppSounds = false; + function playSound(type) { + if (_suppressAppSounds) return; const src = soundMap[type]; if (!src) return; const audio = new Audio(src); @@ -40,6 +43,7 @@ function playSound(type) { } function playSoundUrl(url) { + if (_suppressAppSounds) return; const audio = new Audio(url); audio.volume = 0.5; audio.play().catch(e => console.error("Sound play failed", e)); @@ -60,6 +64,7 @@ export const VoiceProvider = ({ children }) => { parseInt(localStorage.getItem('voiceOutputVolume') || '100') ); const isMovingRef = useRef(false); + const [isReceivingScreenShareAudio, setIsReceivingScreenShareAudio] = useState(false); const convex = useConvex(); @@ -113,7 +118,7 @@ export const VoiceProvider = ({ children }) => { // Apply volume to LiveKit participant (factoring in global output volume) const participant = room?.remoteParticipants?.get(userId); 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 if (volume === 0) { setPersonallyMutedUsers(prev => { @@ -147,7 +152,7 @@ export const VoiceProvider = ({ children }) => { const vol = userVolumes[userId] ?? 100; const restoreVol = vol === 0 ? 100 : vol; 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 if (vol === 0) { 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) => { for (const users of Object.values(voiceStates)) { 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 - const prevChannelUsersRef = useRef(new Map()); + const prevChannelUsersRef = useRef(new Set()); const otherJoinInitRef = useRef(false); const isInAfkChannel = !!(activeChannelId && serverSettings?.afkChannelId === activeChannelId); @@ -378,7 +393,7 @@ export const VoiceProvider = ({ children }) => { participant.setVolume(0); } else { 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 useEffect(() => { if (!activeChannelId) { - prevChannelUsersRef.current = new Map(); + prevChannelUsersRef.current = new Set(); otherJoinInitRef.current = false; return; } const selfId = localStorage.getItem('userId'); const channelUsers = voiceStates[activeChannelId] || []; - const currentUsers = new Map(); - for (const u of channelUsers) { - currentUsers.set(u.userId, u); + const currentUserIds = new Set(channelUsers.map(u => u.userId)); + + // 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 if (!otherJoinInitRef.current) { otherJoinInitRef.current = true; - prevChannelUsersRef.current = currentUsers; + prevChannelUsersRef.current = currentUserIds; return; } - const prev = prevChannelUsersRef.current; + const prevIds = prevChannelUsersRef.current; // Detect new users (not self) - for (const [uid, userData] of currentUsers) { - if (uid !== selfId && !prev.has(uid)) { - if (userData.joinSoundUrl) { + for (const uid of currentUserIds) { + if (uid !== selfId && !prevIds.has(uid)) { + const userData = channelUsers.find(u => u.userId === uid); + if (userData?.joinSoundUrl) { playSoundUrl(userData.joinSoundUrl); } else { playSound('join'); @@ -451,7 +469,7 @@ export const VoiceProvider = ({ children }) => { } } - prevChannelUsersRef.current = currentUsers; + prevChannelUsersRef.current = currentUserIds; }, [voiceStates, activeChannelId]); // Manage screen share subscriptions — only subscribe when actively watching @@ -459,6 +477,7 @@ export const VoiceProvider = ({ children }) => { if (!room) return; const manageSubscriptions = () => { + let receivingAudio = false; for (const p of room.remoteParticipants.values()) { const { screenSharePub, screenShareAudioPub } = findTrackPubs(p); @@ -470,7 +489,13 @@ export const VoiceProvider = ({ children }) => { if (screenShareAudioPub && screenShareAudioPub.isSubscribed !== shouldSubscribe) { screenShareAudioPub.setSubscribed(shouldSubscribe); } + + if (shouldSubscribe && screenShareAudioPub && screenShareAudioPub.isSubscribed) { + receivingAudio = true; + } } + _suppressAppSounds = receivingAudio; + setIsReceivingScreenShareAudio(receivingAudio); }; manageSubscriptions(); @@ -478,10 +503,14 @@ export const VoiceProvider = ({ children }) => { const onTrackChange = () => manageSubscriptions(); room.on(RoomEvent.TrackPublished, onTrackChange); room.on(RoomEvent.TrackSubscribed, onTrackChange); + room.on(RoomEvent.TrackUnsubscribed, onTrackChange); return () => { room.off(RoomEvent.TrackPublished, onTrackChange); room.off(RoomEvent.TrackSubscribed, onTrackChange); + room.off(RoomEvent.TrackUnsubscribed, onTrackChange); + _suppressAppSounds = false; + setIsReceivingScreenShareAudio(false); }; }, [room, watchingStreamOf]); @@ -639,6 +668,7 @@ export const VoiceProvider = ({ children }) => { setUserVolume, getUserVolume, serverMute, + disconnectUser, isServerMuted, isInAfkChannel, serverSettings, @@ -647,6 +677,7 @@ export const VoiceProvider = ({ children }) => { switchDevice, globalOutputVolume, setGlobalOutputVolume, + isReceivingScreenShareAudio, }}> {children} {room && ( diff --git a/packages/shared/src/hooks/useIsMobile.js b/packages/shared/src/hooks/useIsMobile.js new file mode 100644 index 0000000..60ce6a7 --- /dev/null +++ b/packages/shared/src/hooks/useIsMobile.js @@ -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; +} diff --git a/packages/shared/src/hooks/usePresence.js b/packages/shared/src/hooks/usePresence.js new file mode 100644 index 0000000..3a1209b --- /dev/null +++ b/packages/shared/src/hooks/usePresence.js @@ -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]); +} diff --git a/packages/shared/src/hooks/useSingleFlight.js b/packages/shared/src/hooks/useSingleFlight.js new file mode 100644 index 0000000..d2a3c5e --- /dev/null +++ b/packages/shared/src/hooks/useSingleFlight.js @@ -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]); +} diff --git a/packages/shared/src/index.css b/packages/shared/src/index.css index f20438d..894e3f2 100644 --- a/packages/shared/src/index.css +++ b/packages/shared/src/index.css @@ -28,9 +28,14 @@ font-style: normal; } +html { + height: 100%; +} + body { margin: 0; padding: 0; + height: 100%; font-family: "gg sans", "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; background-color: var(--bg-primary); color: var(--text-normal); @@ -1220,15 +1225,15 @@ body { } .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 { - 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 { - 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 { @@ -1255,8 +1260,8 @@ body { } .context-menu-checkbox-indicator.checked { - background-color: hsl(235 86% 65%); - border-color: hsl(235 86% 65%); + background-color: hsl(235, 86%, 65%); + border-color: hsl(235, 86%, 65%); } .context-menu-separator { @@ -1465,6 +1470,7 @@ body { align-items: center; justify-content: center; width: 100%; + margin-top: 8px; margin-bottom: 8px; } @@ -3085,4 +3091,104 @@ body { background-color: rgba(88, 101, 242, 0.15) !important; outline: 2px dashed var(--brand-experiment); 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; + } } \ No newline at end of file diff --git a/packages/shared/src/pages/Chat.jsx b/packages/shared/src/pages/Chat.jsx index f612ce3..7b7c801 100644 --- a/packages/shared/src/pages/Chat.jsx +++ b/packages/shared/src/pages/Chat.jsx @@ -13,9 +13,11 @@ import { useToasts } from '../components/Toast'; import { PresenceProvider } from '../contexts/PresenceContext'; import { getUserPref, setUserPref } from '../utils/userPreferences'; import { usePlatform } from '../platform'; +import { useIsMobile } from '../hooks/useIsMobile'; const Chat = () => { const { crypto, settings } = usePlatform(); + const isMobile = useIsMobile(); const [userId, setUserId] = useState(() => localStorage.getItem('userId')); const [username, setUsername] = useState(() => localStorage.getItem('username') || ''); const [view, setView] = useState(() => { @@ -27,6 +29,7 @@ const Chat = () => { const [activeDMChannel, setActiveDMChannel] = useState(null); const [showMembers, setShowMembers] = useState(true); const [showPinned, setShowPinned] = useState(false); + const [mobileView, setMobileView] = useState('sidebar'); const convex = useConvex(); const { toasts, addToast, removeToast, ToastContainer } = useToasts(); @@ -156,15 +159,21 @@ const Chat = () => { setActiveDMChannel({ channel_id: channelId, other_username: targetUsername }); setView('me'); + if (isMobile) setMobileView('chat'); } catch (err) { console.error('Error opening DM:', err); } - }, [convex]); + }, [convex, isMobile]); const handleSelectChannel = useCallback((channelId) => { setActiveChannel(channelId); setShowPinned(false); + if (isMobile) setMobileView('chat'); + }, [isMobile]); + + const handleMobileBack = useCallback(() => { + setMobileView('sidebar'); }, []); const activeChannelObj = channels.find(c => c._id === activeChannel); @@ -173,6 +182,7 @@ const Chat = () => { const isDMView = view === 'me' && activeDMChannel; const isServerTextChannel = view === 'server' && activeChannel && activeChannelObj?.type !== 'voice'; 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 const isViewingVoiceStage = view === 'server' && activeChannelObj?.type === 'voice' && activeChannel === voiceActiveChannelId; @@ -196,6 +206,8 @@ const Chat = () => { onToggleMembers={() => {}} showMembers={false} onTogglePinned={() => setShowPinned(p => !p)} + isMobile={isMobile} + onMobileBack={handleMobileBack} />
{
); } - return ; + return ( + <> + {isMobile && ( +
+
+ + Friends +
+
+ )} + + + ); } if (activeChannel) { if (activeChannelObj?.type === 'voice') { - return ; + return ( +
+ {isMobile && ( +
+
+ + + + + {activeChannelObj?.name} +
+
+ )} + +
+ ); } return (
@@ -229,9 +272,11 @@ const Chat = () => { channelType="text" channelTopic={activeChannelObj?.topic} onToggleMembers={() => setShowMembers(!showMembers)} - showMembers={showMembers} + showMembers={effectiveShowMembers} onTogglePinned={() => setShowPinned(p => !p)} serverName={serverName} + isMobile={isMobile} + onMobileBack={handleMobileBack} />
{ channelKey={channelKeys[activeChannel]} username={username} userId={userId} - showMembers={showMembers} + showMembers={effectiveShowMembers} onToggleMembers={() => setShowMembers(!showMembers)} onOpenDM={openDM} showPinned={showPinned} @@ -249,7 +294,7 @@ const Chat = () => { /> {}} />
@@ -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) { return (
@@ -276,27 +331,33 @@ const Chat = () => { ); } + const showSidebar = !isMobile || mobileView === 'sidebar'; + const showMainContent = !isMobile || mobileView === 'chat'; + return ( -
- - {renderMainContent()} +
+ {showSidebar && ( + + )} + {showMainContent && renderMainContent()} {showPiP && }
diff --git a/packages/shared/src/styles/themes.css b/packages/shared/src/styles/themes.css index 4c770ac..5d1c545 100644 --- a/packages/shared/src/styles/themes.css +++ b/packages/shared/src/styles/themes.css @@ -50,7 +50,7 @@ --border-muted: rgba(255, 255, 255, 0.04); --border-normal: rgba(255, 255, 255, 0.2); --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 */ --icon-default: #dbdee1; @@ -95,7 +95,7 @@ --background-modifier-selected: rgba(78, 80, 88, 0.6); --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-normal: rgba(0, 0, 0, 0.36); --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 */ --icon-default: #313338; @@ -186,7 +186,7 @@ --background-modifier-selected: rgba(116, 124, 138, 0.30); --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; --channeltextarea-background: #252529; --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; /* Text */ @@ -232,7 +232,7 @@ --border-muted: rgba(255, 255, 255, 0.04); --border-normal: rgba(255, 255, 255, 0.2); --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 */ --icon-default: #dddfe4; @@ -277,7 +277,7 @@ --background-modifier-selected: rgba(78, 80, 88, 0.4); --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-normal: rgba(255, 255, 255, 0.24); --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 */ --icon-default: #e0def0; @@ -368,5 +368,5 @@ --background-modifier-selected: rgba(78, 73, 106, 0.48); --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%); }