From 903aedc8ce0366aa98f180c5e1c210746a646928 Mon Sep 17 00:00:00 2001 From: olcxja Date: Sat, 1 Aug 2026 15:19:27 +0200 Subject: [PATCH] add groups (TEST) --- .../src/main/assets/public/blah/en-cat.json | 464 ++++++----- .../src/main/assets/public/blah/en-us.json | 464 ++++++----- .../src/main/assets/public/blah/pl-pl.json | 464 ++++++----- android/app/src/main/assets/public/index.html | 72 +- android/app/src/main/assets/public/main.js | 766 ++++++++++++++++-- android/app/src/main/assets/public/screens.js | 6 +- android/app/src/main/assets/public/style.css | 19 + webroot/blah/en-cat.json | 464 ++++++----- webroot/blah/en-us.json | 464 ++++++----- webroot/blah/pl-pl.json | 464 ++++++----- webroot/index.html | 72 +- webroot/main.js | 766 ++++++++++++++++-- webroot/screens.js | 6 +- webroot/style.css | 19 + 14 files changed, 3106 insertions(+), 1404 deletions(-) diff --git a/android/app/src/main/assets/public/blah/en-cat.json b/android/app/src/main/assets/public/blah/en-cat.json index e2d7812c..3d8cd6fa 100644 --- a/android/app/src/main/assets/public/blah/en-cat.json +++ b/android/app/src/main/assets/public/blah/en-cat.json @@ -1,205 +1,261 @@ { - "user.not.found": "meowser not found :c", - "dm.already.exists": "direct meowchat already exists :3", - "cant.invite.urself": "you can't invite yourself, silly :3", - "user.already.invited": "you have already invited this user meow", - "user.invited": "user invited successfully :3", - "cant.create.dm.without.invitation": "you can't create a dm without invitation ><", - "dm.begin.notice": "at the beginning of a meowchat, you should always verify that the person you're talking to is the intended recipient", - "failed.accept.dm": "failed to accept direct meowchat", - "dm.accepted": "direct meowchat accepted", - "no.invite.found": "you can't create a dm without invitation *hiss*", - "unknown.error": "unknown error :o", - "forbidden": "you do not have pawmission to perform this action *hiss*", - "dm.key.too.large": "meowchat key too large :o", - "dm.key.not.found": "meowchat key not found :c", - "dm.key.updated": "meowchat key updated successfully :3", - "body.too.large": "Meow is too big! Max meow is {0} MB.", - "message.too.long": "Meowssage exceeds maximum allowed size ({0}).", - "reaction.too.long": "Re-meow-ction exceeds maximum allowed size ({0}).", - "invite.revoked": "invite revoked successfully", - "invite.declined": "invite declined successfully", - "account.creation.request.expired": "cat creation request expired. try again", - "invalid.username": "invalid cat name: {0}", - "invalid.password": "invalid meow word: {0}", - "incorrect.captcha": "incorrect captcha. try meowgain", - "username.taken": "this cat name is already taken", - "accounts.slots.full": "you can't create a new cat because all collars are used. try again later", - "registration.disabled": "registration disabled", - "account.created": "cat created successfully", - "password.changed": "meow word changed successfully", - "keys.updated": "nametag updated successfully", - "unknown.request": "unknown request: {0}", - "login.successful": "pawgin successful :3", - "username.length": "cat name must be {0} characters long", - "username.conditions.allowed": "cat name can only include {all}", - "password.not.hashed.properly": "meow word is not hashed properly", - "invalid.username.or.password": "invalid cat name or meow word", - "error.storage.limit.exceeded": "Storage Limit Exceeded, purr", - "error.network.error": "Network error, check connection or file size, meow.", - "account.not.exist": "cat with this name doesn't exist", - "invalid.nonce": "invalid nonce. try again", - "username.changed": "cat name changed successfully", - "auth.failed.redirect.to.login": "failed to find owner. redirecting to pawgin...", - "dm.refresh.failed": "failed to refresh direct meowchats", - "chat.add.failed": "failed to add meowchat", - "something.wrong.mayb.pass": "something went wrong... :c (probably wrong meow word)", - "something.wrong": "something went wrong... :c", - "passwords.not.match": "meow word do not match", - "password.cant.empty": "meow word cannot be empty", - "username.cant.empty": "cat name cannot be empty", - "bad.request": "bad request!!!", - "letters": "letters", - "numbers": "numbers", - "underscores": "underscores", - "loading.connecting": "connecting...", - "loading.loading": "loading...", - "loading.done": "ready! :3", - "title.home": "bed", - "title.dms": "direct meowchats", - "title.groups": "clowder", - "title.welcome.splash": "welpaw to Meowven", - "desc.welcome.splash": "first Larpix client", - "title.splash": "splash", - "title.create.dm": "invite to direct meowchat", - "title.create.space": "create house", - "title.create.group": "create clowder", - "title.add.chat": "add meowchat", - "desc.add.chat": "add a private, encrypted meowchat by entering a cat name", - "desc.create.group": "create a private, encrypted clowder", - "placeholder.create.group.input": "my clowder", - "placeholder.create.space.input": "my house", - "title.name": "name", - "desc.create.space": "create a house for your community", - "desc.join.space": "join a community that fits you", - "title.join.space": "join house", - "title.space.id": "house id", - "title.inbox": "inbox", - "title.invites": "invites", - "title.notifications": "notifications", - "title.received": "received", - "title.sent": "sent", - "title.all": "all", - "title.unread": "unread", - "desc.no.invites": "no invites found :c", - "desc.no.notifications": "no notifications found :c", - "desc.fetching.invites": "fetching invites...", - "desc.fetching.notifications": "fetching notifications...", - "desc.invite.dm.received": "{0} ({1}) invited you to meowchat", - "desc.invite.dm.sent": "you invited {0} ({1}) to meowchat", - "desc.invite.group.received": "{0} ({1}) invited you to a clowder", - "desc.invite.group.sent": "you invited {0} ({1}) to a clowder", - "action.invite.sending": "Sending mewvite...", - "action.processing": "Purrcessing...", - "action.fetching.invites.sent": "Fetching sent mewvites...", - "action.fetching.invites.recv": "fetching received invites...", - "action.dm.fetch": "fetching direct meowchats...", - "action.auth": "authenticating...", - "action.dm.adding": "adding...", - "action.invite.revoking": "revoking invite...", - "action.invite.accepting": "accepting invite...", - "action.invite.declining": "declining invite...", - "title.sign.up": "sign up", - "title.sign.in": "sign in", - "title.sign.in.to": "sign in to your larpix instance", - "title.server.host": "server host", - "title.username": "meow name", - "title.password": "meow word", - "title.new.here.?": "new here?", - "title.create.an.account": "create a cat", - "join.larpix": "join Larpix", - "title.create.new.account": "create new larpix cat account", - "notice.use.strong.pass": "remember to use a strong meow word! tt will be used to lock your collar", - "title.confirm.password": "confirm meow word", - "title.already.registered.?": "already a kitty?", - "title.back.to.login": "back to pawgin", - "title.verify": "verify", - "title.captcha.desc": "prove you are a kitty", - "title.captcha.code": "captcha code", - "title.invitation.code": "invitation code", - "title.back.to.register": "back to registration", - "title.details": "details", - "title.members": "cats", - "placeholder.username": "cat name", - "placeholder.captcha.code": "captcha code", - "desc.messages.loading": "loading meows...", - "desc.no.dms": "no direct meowchats :c", - "action.dm.opening": "opening meowchat...", - "dm.open.failed": "failed to open meowchat :c", - "keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on", - "keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c", - "dm.messages.fetch.failed": "failed to fetch meows :c", - "messages.decrypt.failed": "failed to decrypt meow :c", - "title.reply.message": "meow back", - "title.react.message": "purr", - "title.text.react": "text purr", - "title.delete.message": "hiss away", - "title.replied.to": "meowed back to", - "title.replying.to": "meowing back to", - "action.message.deleting": "hissing message away...", - "action.message.sending": "sending meow...", - "action.message.reacting": "adding purr...", - "error:message.delete.failed": "failed to hiss away meow", - "error:message.send.failed": "failed to send meow", - "error:message.react.failed": "failed to add purr", - "info.sending.message": "sending...", - "placeholder.message.input": "meow...", - "larp.redacted": "Meow meow was meowed.", - "message.already.deleted": "This meow has already been meowed.", - "rate.limit": "You are meowing too quickly. Please meow again later.", - "placeholder.invitation.code": "meow meow meow", - "info.messages.loading.older": "Loading older meowsages...", - "error:message.not.found": "Meowsage not found", - "title.profile": "cat profile", - "title.settings": "cat settings", - "title.logout": "log out", - "title.loading": "sniffing...", - "action.edit.profile": "edit cat profile", - "action.open.dm": "open direct meowchat", - "action.invite.accept": "accept meow invite", - "action.invite.cancel": "hiss away invite", - "action.invite.send": "invite to meowchat", - "action.profile.loading": "sniffing profile...", - "error.profile.load.failed": "failed to sniff profile.", - "unknown.author": "somecat", - "title.account": "Cat", - "title.delete.account": "Delete cat", - "desc.delete.account": "Once you delete your cat, it will go to cat heaven. Please be certain :c", - "title.avatar": "Cat picture", - "title.banner": "Cat banner", - "title.bio": "Cat bio", - "title.appearance": "Appearance", - "title.language": "Meownguage", - "action.save.profile": "Save cat profile", - "action.save.bio": "Save meow-io", - "action.uploading": "Uploading...", - "action.bio.saving": "Saving meow-io...", - "action.account.saving": "Saving cat...", - "action.account.deleting": "Deleting cat...", - "success:profile.updated": "Cat profile updated successfully :3", - "success:avatar.updated": "Cat picture updated successfully :3", - "success:banner.updated": "Cat banner updated successfully :3", - "action.save": "Save :3", - "action.delete.account": "Delete cat", - "account.deleted.scheduled": "Cat scheduled for deletion. It will go to cat heaven after a month of inactivity. Pawing in again will cancel the deletion process :3", - "action.attachment.take.photo": "Meow photo", - "action.attachment.media": "Meowdia", - "action.attachment.file": "Meowle", - "action.attachment.poll": "Meowll", - "action.attachment.uploading": "Uploading meowttachments...", - "attachment.legacy": "Legacy meowttachment", - "warning.unencrypted": "This meowsage is not encrypted", - "action.change.avatar": "Change Cat Picture", - "action.change.banner": "Change Cat Banner", - "placeholder.type.text.emoji": "Type purr / meow...", - "action.cancel": "Cancel", - "title.crop.image": "Crop Image", - "title.zoom": "Zoom", - "title.pin.message": "Pin", - "title.unpin.message": "Unpin", - "message.pinned.by": "{0} pinned a {1}.", - "message.unpinned.by": "{0} unpinned a {1}.", - "title.pinned.messages": "Pinned", - "error.pinned.messages.empty": "No pinned meows", - "title.message.lowercase": "meowsage" -} \ No newline at end of file + "user.not.found": "meowser not found :c", + "dm.already.exists": "direct meowchat already exists :3", + "cant.invite.urself": "you can't invite yourself, silly :3", + "user.already.invited": "you have already invited this user meow", + "user.invited": "user invited successfully :3", + "cant.create.dm.without.invitation": "you can't create a dm without invitation ><", + "dm.begin.notice": "at the beginning of a meowchat, you should always verify that the person you're talking to is the intended recipient", + "failed.accept.dm": "failed to accept direct meowchat", + "dm.accepted": "direct meowchat accepted", + "no.invite.found": "you can't create a dm without invitation *hiss*", + "unknown.error": "unknown error :o", + "missing.keys": "cannot rotate nametags: some kitties don't have public nametags available yet. they must set up their identity.", + "key.rotated": "meowchat keys have been successfully rotated! meow!", + "forbidden": "you do not have pawmission to perform this action *hiss*", + "dm.key.too.large": "meowchat key too large :o", + "dm.key.not.found": "meowchat key not found :c", + "dm.key.updated": "meowchat key updated successfully :3", + "body.too.large": "Meow is too big! Max meow is {0} MB.", + "message.too.long": "Meowssage exceeds maximum allowed size ({0}).", + "reaction.too.long": "Re-meow-ction exceeds maximum allowed size ({0}).", + "invite.revoked": "invite revoked successfully", + "invite.declined": "invite declined successfully", + "account.creation.request.expired": "cat creation request expired. try again", + "invalid.username": "invalid cat name: {0}", + "invalid.password": "invalid meow word: {0}", + "incorrect.captcha": "incorrect captcha. try meowgain", + "username.taken": "this cat name is already taken", + "accounts.slots.full": "you can't create a new cat because all collars are used. try again later", + "registration.disabled": "registration disabled", + "account.created": "cat created successfully", + "password.changed": "meow word changed successfully", + "keys.updated": "nametag updated successfully", + "unknown.request": "unknown request: {0}", + "login.successful": "pawgin successful :3", + "username.length": "cat name must be {0} characters long", + "username.conditions.allowed": "cat name can only include {all}", + "password.not.hashed.properly": "meow word is not hashed properly", + "invalid.username.or.password": "invalid cat name or meow word", + "error.storage.limit.exceeded": "Storage Limit Exceeded, purr", + "error.network.error": "Network error, check connection or file size, meow.", + "account.not.exist": "cat with this name doesn't exist", + "invalid.nonce": "invalid nonce. try again", + "username.changed": "cat name changed successfully", + "auth.failed.redirect.to.login": "failed to find owner. redirecting to pawgin...", + "dm.refresh.failed": "failed to refresh direct meowchats", + "chat.add.failed": "failed to add meowchat", + "something.wrong.mayb.pass": "something went wrong... :c (probably wrong meow word)", + "something.wrong": "something went wrong... :c", + "passwords.not.match": "meow word do not match", + "password.cant.empty": "meow word cannot be empty", + "username.cant.empty": "cat name cannot be empty", + "bad.request": "bad request!!!", + "letters": "letters", + "numbers": "numbers", + "underscores": "underscores", + "loading.connecting": "connecting...", + "loading.loading": "loading...", + "loading.done": "ready! :3", + "title.home": "bed", + "title.dms": "direct meowchats", + "title.groups": "clowder", + "title.welcome.splash": "welpaw to Meowven", + "desc.welcome.splash": "first Larpix client", + "title.splash": "splash", + "title.create.dm": "invite to direct meowchat", + "title.create.space": "create house", + "title.create.group": "create clowder", + "title.add.chat": "add meowchat", + "desc.add.chat": "add a private, encrypted meowchat by entering a cat name", + "desc.create.group": "create a private, encrypted clowder", + "placeholder.create.group.input": "my clowder", + "placeholder.create.space.input": "my house", + "title.name": "name", + "desc.create.space": "create a house for your community", + "desc.join.space": "join a community that fits you", + "title.join.space": "join house", + "title.space.id": "house id", + "title.inbox": "inbox", + "title.invites": "invites", + "title.notifications": "notifications", + "title.received": "received", + "title.sent": "sent", + "title.all": "all", + "title.unread": "unread", + "desc.no.invites": "no invites found :c", + "desc.no.notifications": "no notifications found :c", + "desc.fetching.invites": "fetching invites...", + "desc.fetching.notifications": "fetching notifications...", + "desc.invite.dm.received": "{0} ({1}) invited you to meowchat", + "desc.invite.dm.sent": "you invited {0} ({1}) to meowchat", + "desc.invite.group.received": "{0} ({1}) invited you to a clowder", + "desc.invite.group.sent": "you invited {0} ({1}) to a clowder", + "action.invite.sending": "Sending mewvite...", + "action.processing": "Purrcessing...", + "action.fetching.invites.sent": "Fetching sent mewvites...", + "action.fetching.invites.recv": "fetching received invites...", + "action.dm.fetch": "fetching direct meowchats...", + "action.auth": "authenticating...", + "action.dm.adding": "adding...", + "action.invite.revoking": "revoking invite...", + "action.invite.accepting": "accepting invite...", + "action.invite.declining": "declining invite...", + "title.sign.up": "sign up", + "title.sign.in": "sign in", + "title.sign.in.to": "sign in to your larpix instance", + "title.server.host": "server host", + "title.username": "meow name", + "title.password": "meow word", + "title.new.here.?": "new here?", + "title.create.an.account": "create a cat", + "join.larpix": "join Larpix", + "title.create.new.account": "create new larpix cat account", + "notice.use.strong.pass": "remember to use a strong meow word! tt will be used to lock your collar", + "title.confirm.password": "confirm meow word", + "title.already.registered.?": "already a kitty?", + "title.back.to.login": "back to pawgin", + "title.verify": "verify", + "title.captcha.desc": "prove you are a kitty", + "title.captcha.code": "captcha code", + "title.invitation.code": "invitation code", + "title.back.to.register": "back to registration", + "title.details": "details", + "title.members": "cats", + "placeholder.username": "cat name", + "placeholder.captcha.code": "captcha code", + "desc.messages.loading": "loading meows...", + "desc.no.dms": "no direct meowchats :c", + "action.dm.opening": "opening meowchat...", + "action.group.opening": "opening clowder...", + "dm.open.failed": "failed to open meowchat :c", + "keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on", + "keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c", + "dm.messages.fetch.failed": "failed to fetch meows :c", + "messages.decrypt.failed": "failed to decrypt meow :c", + "title.reply.message": "meow back", + "title.react.message": "purr", + "title.text.react": "text purr", + "title.delete.message": "hiss away", + "title.replied.to": "meowed back to", + "title.replying.to": "meowing back to", + "action.message.deleting": "hissing message away...", + "action.message.sending": "sending meow...", + "action.message.reacting": "adding purr...", + "error:message.delete.failed": "failed to hiss away meow", + "error:message.send.failed": "failed to send meow", + "error:message.react.failed": "failed to add purr", + "info.sending.message": "sending...", + "placeholder.message.input": "meow...", + "larp.redacted": "Meow meow was meowed.", + "message.already.deleted": "This meow has already been meowed.", + "rate.limit": "You are meowing too quickly. Please meow again later.", + "placeholder.invitation.code": "meow meow meow", + "info.messages.loading.older": "Loading older meowsages...", + "error:message.not.found": "Meowsage not found", + "title.profile": "cat profile", + "title.settings": "cat settings", + "title.logout": "log out", + "title.loading": "sniffing...", + "action.edit.profile": "edit cat profile", + "action.open.dm": "open direct meowchat", + "action.invite.accept": "accept meow invite", + "action.invite.cancel": "hiss away invite", + "action.invite.send": "invite to meowchat", + "action.profile.loading": "sniffing profile...", + "error.profile.load.failed": "failed to sniff profile.", + "unknown.author": "somecat", + "title.account": "Cat", + "title.delete.account": "Delete cat", + "desc.delete.account": "Once you delete your cat, it will go to cat heaven. Please be certain :c", + "title.avatar": "Cat picture", + "title.banner": "Cat banner", + "title.bio": "Cat bio", + "title.appearance": "Appearance", + "title.language": "Meownguage", + "action.save.profile": "Save cat profile", + "action.save.bio": "Save meow-io", + "action.uploading": "Uploading...", + "action.bio.saving": "Saving meow-io...", + "action.account.saving": "Saving cat...", + "action.account.deleting": "Deleting cat...", + "success:profile.updated": "Cat profile updated successfully :3", + "success:avatar.updated": "Cat picture updated successfully :3", + "success:banner.updated": "Cat banner updated successfully :3", + "action.save": "Save :3", + "action.delete.account": "Delete cat", + "account.deleted.scheduled": "Cat scheduled for deletion. It will go to cat heaven after a month of inactivity. Pawing in again will cancel the deletion process :3", + "action.attachment.take.photo": "Meow photo", + "action.attachment.media": "Meowdia", + "action.attachment.file": "Meowle", + "action.attachment.poll": "Meowll", + "action.attachment.uploading": "Uploading meowttachments...", + "attachment.legacy": "Legacy meowttachment", + "warning.unencrypted": "This meowsage is not encrypted", + "action.change.avatar": "Change Cat Picture", + "action.change.banner": "Change Cat Banner", + "placeholder.type.text.emoji": "Type purr / meow...", + "action.cancel": "Cancel", + "title.crop.image": "Crop Image", + "title.zoom": "Zoom", + "title.pin.message": "Pin", + "title.unpin.message": "Unpin", + "message.pinned.by": "{0} pinned a {1}.", + "message.unpinned.by": "{0} unpinned a {1}.", + "title.pinned.messages": "Pinned", + "error.pinned.messages.empty": "No pinned meows", + "title.message.lowercase": "meowsage", + "action.rotate.keys": "Meowtate Keys", + "already.in.group": "Meowready in group", + "desc.key.rotation.info": "Meow need to purrotate meowcription keys to purrtinue sending meowsages in this group. Other meowmbers will autopurrtically receive the new key.", + "desc.no.groups": "Meow are not in any groups yet.", + "dm.read": "Purread", + "empty.body": "Empty meowdy", + "failed.create.group": "Failed to purreate group", + "federation.not.implemented": "Meowderation not implemeowted", + "group.not.found": "Group not meownd", + "group.read": "Purread", + "invalid.entry.name": "Invalid purrtry name", + "invalid.key.index": "Invalid key index (purrlease purrefresh the page)", + "invalid.msg.id": "Invalid meowsage ID", + "invalid.payload": "Invalid purrload", + "invalid.reaction.id": "Invalid purreaction ID", + "invalid.user": "Invalid meowser", + "joined": "Purroined", + "key.rotation.required": "Key purrotation meowquired", + "key.too.large": "Key too meowrge", + "keys.public.mismatch": "Public keys mismemeow", + "label.members": "MEOWMBERS", + "message.deleted": "Meowsage purrleted", + "message.not.found": "Meowsage not meownd", + "message.pin.toggled": "Meowsage purrin purrggled", + "message.reacted": "Meowsage purreacted", + "message.reaction.removed": "Purreaction remeowed", + "no.invite": "No meowvite", + "not.a.member": "Not a meowmber", + "not.in.group": "Not in purroup", + "reaction.not.found": "Purreaction not meownd", + "rotation.not.required": "Purrotation not meowquired", + "storage.entry.updated": "Storage purrtry updated", + "storage.limit.exceeded": "Storage limit exmeowded", + "title.key.rotation": "Key Purrotation", + "error:messages.decrypt.failed": "Depurryption meowled", + "error.message.already.deleted": "Meowsage alpurrdy meowleted", + "group.user.joined.by": "{0} purroined the group", + "group.created.by": "Group purreated by {0}", + "group.user.left.by": "{0} left the purroup", + "action.group.fetch": "Meowtching groups", + "group.created": "Group purreated", + "group.user.joined": "Purroined group", + "prompt.invite.username": "Enter username to invite meow:", + "desc.failed.load.members": "Failed to load members meow", + "prompt.are.you.sure": "Are you sure meow?", + "title.unknown.group": "Unknown Group meow", + "title.unknown": "Unknown meow", + "en-us": "English meow", + "pl-pl": "Polish meow", + "en-cat": "Meowlish", + "avatar.updated": "Avatar updated successfully", + "profile.updated": "Profile updated successfully", + "banner.updated": "Banner updated successfully" +} diff --git a/android/app/src/main/assets/public/blah/en-us.json b/android/app/src/main/assets/public/blah/en-us.json index 21b5fcf2..19aa86ea 100644 --- a/android/app/src/main/assets/public/blah/en-us.json +++ b/android/app/src/main/assets/public/blah/en-us.json @@ -1,205 +1,261 @@ { - "user.not.found": "User not found", - "dm.already.exists": "Direct message already exists", - "cant.invite.urself": "You can't invite yourself", - "user.already.invited": "You have already invited this user", - "user.invited": "User invited successfully", - "cant.create.dm.without.invitation": "You can't create a dm without invitation", - "dm.begin.notice": "At the beginning of a DM, you should always verify that the person you're talking to is the intended recipient", - "failed.accept.dm": "Failed to accept DM", - "dm.accepted": "DM accepted", - "no.invite.found": "You can't create a dm without invitation", - "unknown.error": "Unknown error", - "forbidden": "You do not have permission to perform this action", - "dm.key.too.large": "DM key too large", - "dm.key.not.found": "DM key not found", - "dm.key.updated": "DM key updated successfully", - "body.too.large": "Request too large. Max limit is {0} MB", - "message.too.long": "Message exceeds maximum allowed size ({0}).", - "reaction.too.long": "Reaction exceeds maximum allowed size ({0}).", - "invite.revoked": "Invite revoked successfully", - "invite.declined": "Invite declined successfully", - "account.creation.request.expired": "Account creation request expired. Try again", - "invalid.username": "Invalid username: {0}", - "invalid.password": "Invalid password: {0}", - "incorrect.captcha": "Incorrect captcha. Try again", - "username.taken": "This username is already taken", - "accounts.slots.full": "You can't create a new account because all ids are used. Try again later", - "registration.disabled": "Registration disabled", - "account.created": "Account created successfully", - "password.changed": "Password changed successfully", - "keys.updated": "Keys updated successfully", - "unknown.request": "Unknown request: {0}", - "login.successful": "Login successful", - "username.length": "Username must be {0} characters long", - "username.conditions.allowed": "Username can only include {all}", - "password.not.hashed.properly": "Password is not hashed properly", - "invalid.username.or.password": "Invalid username or password", - "error.storage.limit.exceeded": "Storage Limit Exceeded", - "error.network.error": "Network error, check connection or file size.", - "account.not.exist": "Account with this name doesn't exist", - "invalid.nonce": "Invalid nonce. Try again", - "username.changed": "Username changed successfully", - "auth.failed.redirect.to.login": "Failed to auth. Redirecting to login...", - "dm.refresh.failed": "Failed to refresh dms", - "chat.add.failed": "Failed to add chat", - "something.wrong.mayb.pass": "Something went wrong... (probably wrong password)", - "something.wrong": "Something went wrong...", - "passwords.not.match": "Passwords do not match", - "password.cant.empty": "Password cannot be empty", - "username.cant.empty": "Username cannot be empty", - "bad.request": "Bad request", - "letters": "letters", - "numbers": "numbers", - "underscores": "underscores", - "loading.connecting": "Connecting...", - "loading.loading": "Loading...", - "loading.done": "Ready!", - "title.home": "Home", - "title.dms": "Direct messages", - "title.groups": "Groups", - "title.welcome.splash": "Welcome to Miarven", - "desc.welcome.splash": "First Larpix client", - "title.splash": "Splash", - "title.create.dm": "Invite to dm", - "title.create.space": "Create space", - "title.create.group": "Create group", - "title.add.chat": "Add chat", - "desc.add.chat": "Add a private, encrypted chat by entering a username", - "desc.create.group": "Create a private, encrypted group", - "placeholder.create.group.input": "My group", - "placeholder.create.space.input": "My space", - "title.name": "Name", - "desc.create.space": "Create a space for your community", - "desc.join.space": "Join a community that fits you", - "title.join.space": "Join space", - "title.space.id": "Space id", - "title.inbox": "Inbox", - "title.invites": "Invites", - "title.notifications": "Notifications", - "title.received": "Received", - "title.sent": "Sent", - "title.all": "All", - "title.unread": "Unread", - "desc.no.invites": "No invites found", - "desc.no.notifications": "No notifications found", - "desc.fetching.invites": "Fetching invites...", - "desc.fetching.notifications": "Fetching notifications...", - "desc.invite.dm.received": "{0} ({1}) invited you to chat", - "desc.invite.dm.sent": "You invited {0} ({1}) to chat", - "desc.invite.group.received": "{0} ({1}) invited you to a group", - "desc.invite.group.sent": "You invited {0} ({1}) to a group", - "action.invite.sending": "Sending invite...", - "action.processing": "Processing...", - "action.fetching.invites.sent": "Fetching sent invites...", - "action.fetching.invites.recv": "Fetching received invites...", - "action.dm.fetch": "Fetching dms...", - "action.auth": "Authenticating...", - "action.dm.adding": "Adding...", - "action.invite.revoking": "Revoking invite...", - "action.invite.accepting": "Accepting invite...", - "action.invite.declining": "Declining invite...", - "title.sign.up": "Sign Up", - "title.sign.in": "Sign In", - "title.sign.in.to": "Sign in to your larpix instance", - "title.server.host": "Server Host", - "title.username": "Username", - "title.password": "Password", - "title.new.here.?": "New here?", - "title.create.an.account": "Create an account", - "join.larpix": "Join Larpix", - "title.create.new.account": "Create new larpix account", - "notice.use.strong.pass": "Remember to use a strong password! It will be used to encrypt your keys", - "title.confirm.password": "Confirm Password", - "title.already.registered.?": "Already a member?", - "title.back.to.login": "Back to Login", - "title.verify": "Verify", - "title.captcha.desc": "Prove you are a human", - "title.captcha.code": "Captcha code", - "title.invitation.code": "Invitation code", - "title.back.to.register": "Back to registration", - "placeholder.username": "username", - "placeholder.captcha.code": "captcha code", - "placeholder.message.input": "Message...", - "desc.messages.loading": "Fetching messages...", - "desc.no.dms": "No direct messages", - "action.dm.opening": "Opening DM...", - "dm.open.failed": "Failed to open DM", - "keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.", - "keys.server.decrypt.failed": "Could not decrypt your account keys with this password.", - "dm.messages.fetch.failed": "Failed to fetch messages", - "messages.decrypt.failed": "Failed to decrypt message", - "title.reply.message": "Reply", - "title.react.message": "React", - "title.text.react": "Text React", - "title.delete.message": "Delete", - "title.replied.to": "Replied to", - "title.replying.to": "Replying to", - "action.message.deleting": "Deleting message...", - "action.message.sending": "Sending message...", - "action.message.reacting": "Adding reaction...", - "error:message.delete.failed": "Failed to delete message", - "error:message.send.failed": "Failed to send message", - "error:message.react.failed": "Failed to add reaction", - "info.sending.message": "Sending...", - "larp.redacted": "This message was deleted.", - "message.already.deleted": "This message has already been deleted.", - "rate.limit": "You are sending requests too quickly. Please try again later.", - "placeholder.invitation.code": "invitation code", - "info.messages.loading.older": "Loading older messages...", - "error:message.not.found": "Message not found", - "title.profile": "Profile", - "title.settings": "Settings", - "title.logout": "Log out", - "title.loading": "Loading...", - "title.details": "Details", - "title.members": "Members", - "action.edit.profile": "Edit profile", - "action.open.dm": "Open DM", - "action.invite.accept": "Accept invite", - "action.invite.cancel": "Cancel invite", - "action.invite.send": "Invite to DM", - "action.profile.loading": "Loading profile...", - "error.profile.load.failed": "Failed to load profile.", - "unknown.author": "Someone", - "title.account": "Account", - "title.delete.account": "Delete account", - "desc.delete.account": "Once you delete your account, there is no going back. Please be certain.", - "title.avatar": "Avatar", - "title.banner": "Banner", - "title.bio": "Bio", - "title.appearance": "Appearance", - "title.language": "Language", - "action.save.profile": "Save profile", - "action.save.bio": "Save bio", - "action.uploading": "Uploading...", - "action.bio.saving": "Saving bio...", - "action.account.saving": "Saving account...", - "action.account.deleting": "Deleting account...", - "profile.updated": "Profile updated successfully", - "avatar.updated": "Avatar updated successfully", - "banner.updated": "Banner updated successfully", - "action.save": "Save", - "action.delete.account": "Delete account", - "account.deleted.scheduled": "Account scheduled for deletion. It will be permanently deleted after a month of inactivity. Logging in during this time will cancel the deletion process.", - "action.attachment.take.photo": "Take photo", - "action.attachment.media": "Media", - "action.attachment.file": "File", - "action.attachment.poll": "Poll", - "action.attachment.uploading": "Uploading attachments...", - "attachment.legacy": "Legacy attachment", - "warning.unencrypted": "This message is not encrypted", - "action.change.avatar": "Change Avatar", - "action.change.banner": "Change Banner", - "placeholder.type.text.emoji": "Type text / emoji...", - "action.cancel": "Cancel", - "title.crop.image": "Crop Image", - "title.zoom": "Zoom", - "title.pin.message": "Pin", - "title.unpin.message": "Unpin", - "message.pinned.by": "{0} pinned a {1}.", - "message.unpinned.by": "{0} unpinned a {1}.", - "title.pinned.messages": "Pinned", - "error.pinned.messages.empty": "No pinned messages", - "title.message.lowercase": "message" -} \ No newline at end of file + "user.not.found": "User not found", + "dm.already.exists": "Direct message already exists", + "cant.invite.urself": "You can't invite yourself", + "user.already.invited": "You have already invited this user", + "user.invited": "User invited successfully", + "cant.create.dm.without.invitation": "You can't create a dm without invitation", + "dm.begin.notice": "At the beginning of a DM, you should always verify that the person you're talking to is the intended recipient", + "failed.accept.dm": "Failed to accept DM", + "dm.accepted": "DM accepted", + "no.invite.found": "You can't create a dm without invitation", + "unknown.error": "Unknown error", + "missing.keys": "Cannot rotate keys: Some members don't have public keys available yet. They must configure their identity.", + "key.rotated": "Group keys have been successfully rotated.", + "forbidden": "You do not have permission to perform this action", + "dm.key.too.large": "DM key too large", + "dm.key.not.found": "DM key not found", + "dm.key.updated": "DM key updated successfully", + "body.too.large": "Request too large. Max limit is {0} MB", + "message.too.long": "Message exceeds maximum allowed size ({0}).", + "reaction.too.long": "Reaction exceeds maximum allowed size ({0}).", + "invite.revoked": "Invite revoked successfully", + "invite.declined": "Invite declined successfully", + "account.creation.request.expired": "Account creation request expired. Try again", + "invalid.username": "Invalid username: {0}", + "invalid.password": "Invalid password: {0}", + "incorrect.captcha": "Incorrect captcha. Try again", + "username.taken": "This username is already taken", + "accounts.slots.full": "You can't create a new account because all ids are used. Try again later", + "registration.disabled": "Registration disabled", + "account.created": "Account created successfully", + "password.changed": "Password changed successfully", + "keys.updated": "Keys updated successfully", + "unknown.request": "Unknown request: {0}", + "login.successful": "Login successful", + "username.length": "Username must be {0} characters long", + "username.conditions.allowed": "Username can only include {all}", + "password.not.hashed.properly": "Password is not hashed properly", + "invalid.username.or.password": "Invalid username or password", + "error.storage.limit.exceeded": "Storage Limit Exceeded", + "error.network.error": "Network error, check connection or file size.", + "account.not.exist": "Account with this name doesn't exist", + "invalid.nonce": "Invalid nonce. Try again", + "username.changed": "Username changed successfully", + "auth.failed.redirect.to.login": "Failed to auth. Redirecting to login...", + "dm.refresh.failed": "Failed to refresh dms", + "chat.add.failed": "Failed to add chat", + "something.wrong.mayb.pass": "Something went wrong... (probably wrong password)", + "something.wrong": "Something went wrong...", + "passwords.not.match": "Passwords do not match", + "password.cant.empty": "Password cannot be empty", + "username.cant.empty": "Username cannot be empty", + "bad.request": "Bad request", + "letters": "letters", + "numbers": "numbers", + "underscores": "underscores", + "loading.connecting": "Connecting...", + "loading.loading": "Loading...", + "loading.done": "Ready!", + "title.home": "Home", + "title.dms": "Direct messages", + "title.groups": "Groups", + "title.welcome.splash": "Welcome to Miarven", + "desc.welcome.splash": "First Larpix client", + "title.splash": "Splash", + "title.create.dm": "Invite to dm", + "title.create.space": "Create space", + "title.create.group": "Create group", + "title.add.chat": "Add chat", + "desc.add.chat": "Add a private, encrypted chat by entering a username", + "desc.create.group": "Create a private, encrypted group", + "placeholder.create.group.input": "My group", + "placeholder.create.space.input": "My space", + "title.name": "Name", + "desc.create.space": "Create a space for your community", + "desc.join.space": "Join a community that fits you", + "title.join.space": "Join space", + "title.space.id": "Space id", + "title.inbox": "Inbox", + "title.invites": "Invites", + "title.notifications": "Notifications", + "title.received": "Received", + "title.sent": "Sent", + "title.all": "All", + "title.unread": "Unread", + "desc.no.invites": "No invites found", + "desc.no.notifications": "No notifications found", + "desc.fetching.invites": "Fetching invites...", + "desc.fetching.notifications": "Fetching notifications...", + "desc.invite.dm.received": "{0} ({1}) invited you to chat", + "desc.invite.dm.sent": "You invited {0} ({1}) to chat", + "desc.invite.group.received": "{0} ({1}) invited you to a group", + "desc.invite.group.sent": "You invited {0} ({1}) to a group", + "action.invite.sending": "Sending invite...", + "action.processing": "Processing...", + "action.fetching.invites.sent": "Fetching sent invites...", + "action.fetching.invites.recv": "Fetching received invites...", + "action.dm.fetch": "Fetching dms...", + "action.auth": "Authenticating...", + "action.dm.adding": "Adding...", + "action.invite.revoking": "Revoking invite...", + "action.invite.accepting": "Accepting invite...", + "action.invite.declining": "Declining invite...", + "title.sign.up": "Sign Up", + "title.sign.in": "Sign In", + "title.sign.in.to": "Sign in to your larpix instance", + "title.server.host": "Server Host", + "title.username": "Username", + "title.password": "Password", + "title.new.here.?": "New here?", + "title.create.an.account": "Create an account", + "join.larpix": "Join Larpix", + "title.create.new.account": "Create new larpix account", + "notice.use.strong.pass": "Remember to use a strong password! It will be used to encrypt your keys", + "title.confirm.password": "Confirm Password", + "title.already.registered.?": "Already a member?", + "title.back.to.login": "Back to Login", + "title.verify": "Verify", + "title.captcha.desc": "Prove you are a human", + "title.captcha.code": "Captcha code", + "title.invitation.code": "Invitation code", + "title.back.to.register": "Back to registration", + "placeholder.username": "username", + "placeholder.captcha.code": "captcha code", + "placeholder.message.input": "Message...", + "desc.messages.loading": "Fetching messages...", + "desc.no.dms": "No direct messages", + "action.dm.opening": "Opening DM...", + "action.group.opening": "Opening group...", + "dm.open.failed": "Failed to open DM", + "keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.", + "keys.server.decrypt.failed": "Could not decrypt your account keys with this password.", + "dm.messages.fetch.failed": "Failed to fetch messages", + "messages.decrypt.failed": "Failed to decrypt message", + "title.reply.message": "Reply", + "title.react.message": "React", + "title.text.react": "Text React", + "title.delete.message": "Delete", + "title.replied.to": "Replied to", + "title.replying.to": "Replying to", + "action.message.deleting": "Deleting message...", + "action.message.sending": "Sending message...", + "action.message.reacting": "Adding reaction...", + "error:message.delete.failed": "Failed to delete message", + "error:message.send.failed": "Failed to send message", + "error:message.react.failed": "Failed to add reaction", + "info.sending.message": "Sending...", + "larp.redacted": "This message was deleted.", + "message.already.deleted": "This message has already been deleted.", + "rate.limit": "You are sending requests too quickly. Please try again later.", + "placeholder.invitation.code": "invitation code", + "info.messages.loading.older": "Loading older messages...", + "error:message.not.found": "Message not found", + "title.profile": "Profile", + "title.settings": "Settings", + "title.logout": "Log out", + "title.loading": "Loading...", + "title.details": "Details", + "title.members": "Members", + "action.edit.profile": "Edit profile", + "action.open.dm": "Open DM", + "action.invite.accept": "Accept invite", + "action.invite.cancel": "Cancel invite", + "action.invite.send": "Invite to DM", + "action.profile.loading": "Loading profile...", + "error.profile.load.failed": "Failed to load profile.", + "unknown.author": "Someone", + "title.account": "Account", + "title.delete.account": "Delete account", + "desc.delete.account": "Once you delete your account, there is no going back. Please be certain.", + "title.avatar": "Avatar", + "title.banner": "Banner", + "title.bio": "Bio", + "title.appearance": "Appearance", + "title.language": "Language", + "action.save.profile": "Save profile", + "action.save.bio": "Save bio", + "action.uploading": "Uploading...", + "action.bio.saving": "Saving bio...", + "action.account.saving": "Saving account...", + "action.account.deleting": "Deleting account...", + "profile.updated": "Profile updated successfully", + "avatar.updated": "Avatar updated successfully", + "banner.updated": "Banner updated successfully", + "action.save": "Save", + "action.delete.account": "Delete account", + "account.deleted.scheduled": "Account scheduled for deletion. It will be permanently deleted after a month of inactivity. Logging in during this time will cancel the deletion process.", + "action.attachment.take.photo": "Take photo", + "action.attachment.media": "Media", + "action.attachment.file": "File", + "action.attachment.poll": "Poll", + "action.attachment.uploading": "Uploading attachments...", + "attachment.legacy": "Legacy attachment", + "warning.unencrypted": "This message is not encrypted", + "action.change.avatar": "Change Avatar", + "action.change.banner": "Change Banner", + "placeholder.type.text.emoji": "Type text / emoji...", + "action.cancel": "Cancel", + "title.crop.image": "Crop Image", + "title.zoom": "Zoom", + "title.pin.message": "Pin", + "title.unpin.message": "Unpin", + "message.pinned.by": "{0} pinned a {1}.", + "message.unpinned.by": "{0} unpinned a {1}.", + "title.pinned.messages": "Pinned", + "error.pinned.messages.empty": "No pinned messages", + "title.message.lowercase": "message", + "action.rotate.keys": "Rotate Keys", + "already.in.group": "Already in group", + "desc.key.rotation.info": "You need to rotate encryption keys to continue sending messages in this group. Other members will automatically receive the new key.", + "desc.no.groups": "You are not in any groups yet.", + "dm.read": "Read", + "empty.body": "Empty body", + "failed.create.group": "Failed to create group", + "federation.not.implemented": "Federation not implemented", + "group.not.found": "Group not found", + "group.read": "Read", + "invalid.entry.name": "Invalid entry name", + "invalid.key.index": "Invalid key index (please refresh the page)", + "invalid.msg.id": "Invalid message ID", + "invalid.payload": "Invalid payload", + "invalid.reaction.id": "Invalid reaction ID", + "invalid.user": "Invalid user", + "joined": "Joined", + "key.rotation.required": "Key rotation required", + "key.too.large": "Key too large", + "keys.public.mismatch": "Public keys mismatch", + "label.members": "MEMBERS", + "message.deleted": "Message deleted", + "message.not.found": "Message not found", + "message.pin.toggled": "Message pin toggled", + "message.reacted": "Message reacted", + "message.reaction.removed": "Reaction removed", + "no.invite": "No invite", + "not.a.member": "Not a member", + "not.in.group": "Not in group", + "reaction.not.found": "Reaction not found", + "rotation.not.required": "Rotation not required", + "storage.entry.updated": "Storage entry updated", + "storage.limit.exceeded": "Storage limit exceeded", + "title.key.rotation": "Key Rotation", + "error:messages.decrypt.failed": "Decryption failed", + "error.message.already.deleted": "Message already deleted", + "group.user.joined.by": "{0} joined the group", + "group.created.by": "Group created by {0}", + "group.user.left.by": "{0} left the group", + "action.group.fetch": "Fetching groups", + "group.created": "Group created", + "group.user.joined": "Joined group", + "prompt.invite.username": "Enter username to invite:", + "desc.failed.load.members": "Failed to load members", + "prompt.are.you.sure": "Are you sure?", + "title.unknown.group": "Unknown Group", + "title.unknown": "Unknown", + "success:banner.updated": "Banner updated successfully", + "success:profile.updated": "Profile updated successfully", + "success:avatar.updated": "Avatar updated successfully", + "en-us": "English", + "pl-pl": "Polish", + "en-cat": "Meowlish" +} diff --git a/android/app/src/main/assets/public/blah/pl-pl.json b/android/app/src/main/assets/public/blah/pl-pl.json index 7ab7e4bf..302197a2 100644 --- a/android/app/src/main/assets/public/blah/pl-pl.json +++ b/android/app/src/main/assets/public/blah/pl-pl.json @@ -1,205 +1,261 @@ { - "user.not.found": "Nie znaleziono użytkownika", - "dm.already.exists": "Wiadomość prywatna już istnieje", - "cant.invite.urself": "Nie możesz zaprosić samego siebie", - "user.already.invited": "Ten użytkownik został już przez Ciebie zaproszony", - "user.invited": "Użytkownik został pomyślnie zaproszony", - "cant.create.dm.without.invitation": "Nie możesz utworzyć wiadomości prywatnej bez zaproszenia", - "dm.begin.notice": "Na początku każdej rozmowy prywatnej zawsze upewnij się, że rozmawiasz z właściwą osobą", - "failed.accept.dm": "Nie udało się zaakceptować rozmowy prywatnej", - "dm.accepted": "Rozmowa prywatna została zaakceptowana", - "no.invite.found": "Nie możesz utworzyć wiadomości prywatnej bez zaproszenia", - "unknown.error": "Nieznany błąd", - "forbidden": "Nie masz uprawnień do wykonania tej akcji", - "dm.key.too.large": "Klucz wiadomości prywatnej jest zbyt duży", - "dm.key.not.found": "Nie znaleziono klucza wiadomości prywatnej", - "dm.key.updated": "Klucz wiadomości prywatnej został pomyślnie zaktualizowany", - "body.too.large": "Treść żądania jest zbyt duża. Maksymalny limit to {0} MB", - "message.too.long": "Wiadomość przekracza dozwolony rozmiar ({0}).", - "reaction.too.long": "Reakcja przekracza dozwolony rozmiar ({0}).", - "invite.revoked": "Zaproszenie zostało cofnięte", - "invite.declined": "Zaproszenie zostało odrzucone", - "account.creation.request.expired": "Żądanie utworzenia konta wygasło. Spróbuj ponownie", - "invalid.username": "Nieprawidłowa nazwa użytkownika: {0}", - "invalid.password": "Nieprawidłowe hasło: {0}", - "incorrect.captcha": "Nieprawidłowa captcha. Spróbuj ponownie", - "username.taken": "Ta nazwa użytkownika jest już zajęta", - "accounts.slots.full": "Nie można utworzyć nowego konta, ponieważ wszystkie identyfikatory są zajęte. Spróbuj ponownie później", - "registration.disabled": "Rejestracja jest wyłączona", - "account.created": "Konto zostało pomyślnie utworzone", - "password.changed": "Hasło zostało pomyślnie zmienione", - "keys.updated": "Klucze zostały pomyślnie zaktualizowane", - "unknown.request": "Nieznane żądanie: {0}", - "login.successful": "Logowanie zakończone pomyślnie", - "username.length": "Nazwa użytkownika musi mieć {0} znaków", - "username.conditions.allowed": "Nazwa użytkownika może zawierać tylko: {all}", - "password.not.hashed.properly": "Hasło nie zostało poprawnie zahaszowane", - "invalid.username.or.password": "Nieprawidłowa nazwa użytkownika lub hasło", - "error.storage.limit.exceeded": "Przekroczono limit pamięci", - "error.network.error": "Błąd sieci. Sprawdź połączenie lub rozmiar pliku.", - "account.not.exist": "Konto o tej nazwie nie istnieje", - "invalid.nonce": "Nieprawidłowy nonce. Spróbuj ponownie", - "username.changed": "Nazwa użytkownika została pomyślnie zmieniona", - "auth.failed.redirect.to.login": "Uwierzytelnianie nie powiodło się. Przekierowywanie do logowania...", - "dm.refresh.failed": "Nie udało się odświeżyć rozmów prywatnych", - "chat.add.failed": "Nie udało się dodać czatu", - "something.wrong.mayb.pass": "Coś poszło nie tak... (prawdopodobnie błędne hasło)", - "something.wrong": "Coś poszło nie tak...", - "passwords.not.match": "Hasła nie są zgodne", - "password.cant.empty": "Hasło nie może być puste", - "username.cant.empty": "Nazwa użytkownika nie może być pusta", - "bad.request": "Nieprawidłowe żądanie", - "letters": "litery", - "numbers": "cyfry", - "underscores": "podkreślenia", - "loading.connecting": "Łączenie...", - "loading.loading": "Ładowanie...", - "loading.done": "Gotowe!", - "title.home": "Strona główna", - "title.dms": "Wiadomości prywatne", - "title.groups": "Grupy", - "title.welcome.splash": "Witamy w Miarven", - "desc.welcome.splash": "Pierwszy klient Larpix", - "title.splash": "Ekran powitalny", - "title.create.dm": "Zaproś do rozmowy prywatnej", - "title.create.space": "Utwórz społeczność", - "title.create.group": "Utwórz grupę", - "title.add.chat": "Dodaj czat", - "desc.add.chat": "Dodaj prywatny, szyfrowany czat, wpisując nazwę użytkownika", - "desc.create.group": "Utwórz prywatną, szyfrowaną grupę", - "placeholder.create.group.input": "Moja grupa", - "placeholder.create.space.input": "Moja społeczność", - "title.name": "Nazwa", - "desc.create.space": "Utwórz społeczność dla swojej grupy", - "desc.join.space": "Dołącz do społeczności, która Ci odpowiada", - "title.join.space": "Dołącz do społeczności", - "title.space.id": "Identyfikator społeczności", - "title.inbox": "Skrzynka odbiorcza", - "title.invites": "Zaproszenia", - "title.notifications": "Powiadomienia", - "title.received": "Odebrane", - "title.sent": "Wysłane", - "title.all": "Wszystkie", - "title.unread": "Nieprzeczytane", - "desc.no.invites": "Brak zaproszeń", - "desc.no.notifications": "Brak powiadomień", - "desc.fetching.invites": "Pobieranie zaproszeń...", - "desc.fetching.notifications": "Pobieranie powiadomień...", - "desc.invite.dm.received": "{0} ({1}) zaprosił(a) Cię do rozmowy", - "desc.invite.dm.sent": "Zaprosiłeś(-aś) {0} ({1}) do rozmowy", - "desc.invite.group.received": "{0} ({1}) zaprosił(a) Cię do grupy", - "desc.invite.group.sent": "Zaprosiłeś(-aś) {0} ({1}) do grupy", - "action.invite.sending": "Wysyłanie zaproszenia...", - "action.processing": "Przetwarzanie...", - "action.fetching.invites.sent": "Pobieranie wysłanych zaproszeń...", - "action.fetching.invites.recv": "Pobieranie otrzymanych zaproszeń...", - "action.dm.fetch": "Pobieranie rozmów prywatnych...", - "action.auth": "Uwierzytelnianie...", - "action.dm.adding": "Dodawanie...", - "action.invite.revoking": "Cofanie zaproszenia...", - "action.invite.accepting": "Akceptowanie zaproszenia...", - "action.invite.declining": "Odrzucanie zaproszenia...", - "title.sign.up": "Zarejestruj się", - "title.sign.in": "Zaloguj się", - "title.sign.in.to": "Zaloguj się do swojej instancji Larpix", - "title.server.host": "Adres serwera", - "title.username": "Nazwa użytkownika", - "title.password": "Hasło", - "title.new.here.?": "Nowy tutaj?", - "title.create.an.account": "Utwórz konto", - "join.larpix": "Dołącz do Larpix", - "title.create.new.account": "Utwórz nowe konto Larpix", - "notice.use.strong.pass": "Pamiętaj o użyciu silnego hasła! Posłuży ono do szyfrowania Twoich kluczy.", - "title.confirm.password": "Potwierdź hasło", - "title.already.registered.?": "Masz już konto?", - "title.back.to.login": "Powrót do logowania", - "title.verify": "Weryfikacja", - "title.captcha.desc": "Udowodnij, że jesteś człowiekiem", - "title.captcha.code": "Kod CAPTCHA", - "title.invitation.code": "Kod zaproszenia", - "title.back.to.register": "Powrót do rejestracji", - "placeholder.username": "nazwa użytkownika", - "placeholder.captcha.code": "kod captcha", - "placeholder.message.input": "Wiadomość...", - "desc.messages.loading": "Pobieranie wiadomości...", - "desc.no.dms": "Brak wiadomości prywatnych", - "action.dm.opening": "Otwieranie rozmowy...", - "dm.open.failed": "Nie udało się otworzyć rozmowy", - "keys.local.server.mismatch": "To urządzenie ma inne klucze szyfrowania niż serwer. Użyj urządzenia, na którym zalogowano się po raz pierwszy, lub przywróć klucze z kopii zapasowej.", - "keys.server.decrypt.failed": "Nie udało się odszyfrować kluczy konta przy użyciu tego hasła.", - "dm.messages.fetch.failed": "Nie udało się pobrać wiadomości", - "messages.decrypt.failed": "Nie udało się odszyfrować wiadomości", - "title.reply.message": "Odpowiedz", - "title.react.message": "Zareaguj", - "title.text.react": "Reakcja tekstowa", - "title.delete.message": "Usuń", - "title.replied.to": "W odpowiedzi do", - "title.replying.to": "Odpowiadasz na", - "action.message.deleting": "Usuwanie wiadomości...", - "action.message.sending": "Wysyłanie wiadomości...", - "action.message.reacting": "Dodawanie reakcji...", - "error:message.delete.failed": "Nie udało się usunąć wiadomości", - "error:message.send.failed": "Nie udało się wysłać wiadomości", - "error:message.react.failed": "Nie udało się dodać reakcji", - "info.sending.message": "Wysyłanie...", - "larp.redacted": "Ta wiadomość została usunięta.", - "message.already.deleted": "Ta wiadomość została już usunięta.", - "rate.limit": "Wysyłasz żądania zbyt szybko. Spróbuj ponownie później.", - "placeholder.invitation.code": "kod zaproszenia", - "info.messages.loading.older": "Ładowanie starszych wiadomości...", - "error:message.not.found": "Nie znaleziono wiadomości", - "title.profile": "Profil", - "title.settings": "Ustawienia", - "title.logout": "Wyloguj", - "title.loading": "Ładowanie...", - "title.details": "Szczegóły", - "title.members": "Członkowie", - "action.edit.profile": "Edytuj profil", - "action.open.dm": "Otwórz rozmowę", - "action.invite.accept": "Akceptuj zaproszenie", - "action.invite.cancel": "Anuluj zaproszenie", - "action.invite.send": "Zaproś do rozmowy", - "action.profile.loading": "Ładowanie profilu...", - "error.profile.load.failed": "Nie udało się załadować profilu.", - "unknown.author": "Ktoś", - "title.account": "Konto", - "title.delete.account": "Usuń konto", - "desc.delete.account": "Po usunięciu konta nie będzie możliwości jego odzyskania. Upewnij się, że na pewno chcesz to zrobić.", - "title.avatar": "Awatar", - "title.banner": "Baner", - "title.bio": "Opis", - "title.appearance": "Wygląd", - "title.language": "Język", - "action.save.profile": "Zapisz profil", - "action.save.bio": "Zapisz opis", - "action.uploading": "Przesyłanie...", - "action.bio.saving": "Zapisywanie opisu...", - "action.account.saving": "Zapisywanie konta...", - "action.account.deleting": "Usuwanie konta...", - "profile.updated": "Profil został pomyślnie zaktualizowany", - "avatar.updated": "Awatar został pomyślnie zaktualizowany", - "banner.updated": "Baner został pomyślnie zaktualizowany", - "action.save": "Zapisz", - "action.delete.account": "Usuń konto", - "account.deleted.scheduled": "Konto zostało oznaczone do usunięcia. Zostanie trwale usunięte po miesiącu braku aktywności. Zalogowanie się w tym czasie anuluje proces usuwania.", - "action.attachment.take.photo": "Zrób zdjęcie", - "action.attachment.media": "Multimedia", - "action.attachment.file": "Plik", - "action.attachment.poll": "Ankieta", - "action.attachment.uploading": "Przesyłanie załączników...", - "attachment.legacy": "Starszy załącznik", - "warning.unencrypted": "Ta wiadomość nie jest szyfrowana", - "action.change.avatar": "Zmień awatar", - "action.change.banner": "Zmień baner", - "placeholder.type.text.emoji": "Wpisz tekst / emoji...", - "action.cancel": "Anuluj", - "title.crop.image": "Przytnij obraz", - "title.zoom": "Przybliżenie", - "title.pin.message": "Przypnij", - "title.unpin.message": "Odepnij", - "message.pinned.by": "{0} przypiął {1}.", - "message.unpinned.by": "{0} odpiął {1}.", - "title.pinned.messages": "Przypięte", - "error.pinned.messages.empty": "Brak przypiętych wiadomości", - "title.message.lowercase": "wiadomość" -} \ No newline at end of file + "user.not.found": "Nie znaleziono użytkownika", + "dm.already.exists": "Wiadomość prywatna już istnieje", + "cant.invite.urself": "Nie możesz zaprosić samego siebie", + "user.already.invited": "Ten użytkownik został już przez Ciebie zaproszony", + "user.invited": "Użytkownik został pomyślnie zaproszony", + "cant.create.dm.without.invitation": "Nie możesz utworzyć wiadomości prywatnej bez zaproszenia", + "dm.begin.notice": "Na początku każdej rozmowy prywatnej zawsze upewnij się, że rozmawiasz z właściwą osobą", + "failed.accept.dm": "Nie udało się zaakceptować rozmowy prywatnej", + "dm.accepted": "Rozmowa prywatna została zaakceptowana", + "no.invite.found": "Nie możesz utworzyć wiadomości prywatnej bez zaproszenia", + "unknown.error": "Nieznany błąd", + "forbidden": "Nie masz uprawnień do wykonania tej akcji", + "dm.key.too.large": "Klucz wiadomości prywatnej jest zbyt duży", + "dm.key.not.found": "Nie znaleziono klucza wiadomości prywatnej", + "dm.key.updated": "Klucz wiadomości prywatnej został pomyślnie zaktualizowany", + "body.too.large": "Treść żądania jest zbyt duża. Maksymalny limit to {0} MB", + "message.too.long": "Wiadomość przekracza dozwolony rozmiar ({0}).", + "reaction.too.long": "Reakcja przekracza dozwolony rozmiar ({0}).", + "invite.revoked": "Zaproszenie zostało cofnięte", + "invite.declined": "Zaproszenie zostało odrzucone", + "account.creation.request.expired": "Żądanie utworzenia konta wygasło. Spróbuj ponownie", + "invalid.username": "Nieprawidłowa nazwa użytkownika: {0}", + "invalid.password": "Nieprawidłowe hasło: {0}", + "incorrect.captcha": "Nieprawidłowa captcha. Spróbuj ponownie", + "username.taken": "Ta nazwa użytkownika jest już zajęta", + "accounts.slots.full": "Nie można utworzyć nowego konta, ponieważ wszystkie identyfikatory są zajęte. Spróbuj ponownie później", + "registration.disabled": "Rejestracja jest wyłączona", + "account.created": "Konto zostało pomyślnie utworzone", + "password.changed": "Hasło zostało pomyślnie zmienione", + "keys.updated": "Klucze zostały pomyślnie zaktualizowane", + "unknown.request": "Nieznane żądanie: {0}", + "login.successful": "Logowanie zakończone pomyślnie", + "username.length": "Nazwa użytkownika musi mieć {0} znaków", + "username.conditions.allowed": "Nazwa użytkownika może zawierać tylko: {all}", + "password.not.hashed.properly": "Hasło nie zostało poprawnie zahaszowane", + "invalid.username.or.password": "Nieprawidłowa nazwa użytkownika lub hasło", + "error.storage.limit.exceeded": "Przekroczono limit pamięci", + "error.network.error": "Błąd sieci. Sprawdź połączenie lub rozmiar pliku.", + "account.not.exist": "Konto o tej nazwie nie istnieje", + "invalid.nonce": "Nieprawidłowy nonce. Spróbuj ponownie", + "username.changed": "Nazwa użytkownika została pomyślnie zmieniona", + "auth.failed.redirect.to.login": "Uwierzytelnianie nie powiodło się. Przekierowywanie do logowania...", + "dm.refresh.failed": "Nie udało się odświeżyć rozmów prywatnych", + "chat.add.failed": "Nie udało się dodać czatu", + "something.wrong.mayb.pass": "Coś poszło nie tak... (prawdopodobnie błędne hasło)", + "something.wrong": "Coś poszło nie tak...", + "passwords.not.match": "Hasła nie są zgodne", + "password.cant.empty": "Hasło nie może być puste", + "username.cant.empty": "Nazwa użytkownika nie może być pusta", + "bad.request": "Nieprawidłowe żądanie", + "letters": "litery", + "numbers": "cyfry", + "underscores": "podkreślenia", + "loading.connecting": "Łączenie...", + "loading.loading": "Ładowanie...", + "loading.done": "Gotowe!", + "title.home": "Strona główna", + "title.dms": "Wiadomości prywatne", + "title.groups": "Grupy", + "title.welcome.splash": "Witamy w Miarven", + "desc.welcome.splash": "Pierwszy klient Larpix", + "title.splash": "Ekran powitalny", + "title.create.dm": "Zaproś do rozmowy prywatnej", + "title.create.space": "Utwórz społeczność", + "title.create.group": "Utwórz grupę", + "title.add.chat": "Dodaj czat", + "desc.add.chat": "Dodaj prywatny, szyfrowany czat, wpisując nazwę użytkownika", + "desc.create.group": "Utwórz prywatną, szyfrowaną grupę", + "placeholder.create.group.input": "Moja grupa", + "placeholder.create.space.input": "Moja społeczność", + "title.name": "Nazwa", + "desc.create.space": "Utwórz społeczność dla swojej grupy", + "desc.join.space": "Dołącz do społeczności, która Ci odpowiada", + "title.join.space": "Dołącz do społeczności", + "title.space.id": "Identyfikator społeczności", + "title.inbox": "Skrzynka odbiorcza", + "title.invites": "Zaproszenia", + "title.notifications": "Powiadomienia", + "title.received": "Odebrane", + "title.sent": "Wysłane", + "title.all": "Wszystkie", + "title.unread": "Nieprzeczytane", + "desc.no.invites": "Brak zaproszeń", + "desc.no.notifications": "Brak powiadomień", + "desc.fetching.invites": "Pobieranie zaproszeń...", + "desc.fetching.notifications": "Pobieranie powiadomień...", + "desc.invite.dm.received": "{0} ({1}) zaprosił(a) Cię do rozmowy", + "desc.invite.dm.sent": "Zaprosiłeś(-aś) {0} ({1}) do rozmowy", + "desc.invite.group.received": "{0} ({1}) zaprosił(a) Cię do grupy", + "desc.invite.group.sent": "Zaprosiłeś(-aś) {0} ({1}) do grupy", + "action.invite.sending": "Wysyłanie zaproszenia...", + "action.processing": "Przetwarzanie...", + "action.fetching.invites.sent": "Pobieranie wysłanych zaproszeń...", + "action.fetching.invites.recv": "Pobieranie otrzymanych zaproszeń...", + "action.dm.fetch": "Pobieranie rozmów prywatnych...", + "action.auth": "Uwierzytelnianie...", + "action.dm.adding": "Dodawanie...", + "action.invite.revoking": "Cofanie zaproszenia...", + "action.invite.accepting": "Akceptowanie zaproszenia...", + "action.invite.declining": "Odrzucanie zaproszenia...", + "title.sign.up": "Zarejestruj się", + "title.sign.in": "Zaloguj się", + "title.sign.in.to": "Zaloguj się do swojej instancji Larpix", + "title.server.host": "Adres serwera", + "title.username": "Nazwa użytkownika", + "title.password": "Hasło", + "title.new.here.?": "Nowy tutaj?", + "title.create.an.account": "Utwórz konto", + "join.larpix": "Dołącz do Larpix", + "title.create.new.account": "Utwórz nowe konto Larpix", + "notice.use.strong.pass": "Pamiętaj o użyciu silnego hasła! Posłuży ono do szyfrowania Twoich kluczy.", + "title.confirm.password": "Potwierdź hasło", + "title.already.registered.?": "Masz już konto?", + "title.back.to.login": "Powrót do logowania", + "title.verify": "Weryfikacja", + "title.captcha.desc": "Udowodnij, że jesteś człowiekiem", + "title.captcha.code": "Kod CAPTCHA", + "title.invitation.code": "Kod zaproszenia", + "title.back.to.register": "Powrót do rejestracji", + "placeholder.username": "nazwa użytkownika", + "placeholder.captcha.code": "kod captcha", + "placeholder.message.input": "Wiadomość...", + "desc.messages.loading": "Pobieranie wiadomości...", + "desc.no.dms": "Brak wiadomości prywatnych", + "action.dm.opening": "Otwieranie rozmowy...", + "action.group.opening": "Otwieranie grupy...", + "dm.open.failed": "Nie udało się otworzyć rozmowy", + "keys.local.server.mismatch": "To urządzenie ma inne klucze szyfrowania niż serwer. Użyj urządzenia, na którym zalogowano się po raz pierwszy, lub przywróć klucze z kopii zapasowej.", + "keys.server.decrypt.failed": "Nie udało się odszyfrować kluczy konta przy użyciu tego hasła.", + "dm.messages.fetch.failed": "Nie udało się pobrać wiadomości", + "messages.decrypt.failed": "Nie udało się odszyfrować wiadomości", + "title.reply.message": "Odpowiedz", + "title.react.message": "Zareaguj", + "title.text.react": "Reakcja tekstowa", + "title.delete.message": "Usuń", + "title.replied.to": "W odpowiedzi do", + "title.replying.to": "Odpowiadasz na", + "action.message.deleting": "Usuwanie wiadomości...", + "action.message.sending": "Wysyłanie wiadomości...", + "action.message.reacting": "Dodawanie reakcji...", + "error:message.delete.failed": "Nie udało się usunąć wiadomości", + "error:message.send.failed": "Nie udało się wysłać wiadomości", + "error:message.react.failed": "Nie udało się dodać reakcji", + "info.sending.message": "Wysyłanie...", + "larp.redacted": "Ta wiadomość została usunięta.", + "message.already.deleted": "Ta wiadomość została już usunięta.", + "rate.limit": "Wysyłasz żądania zbyt szybko. Spróbuj ponownie później.", + "placeholder.invitation.code": "kod zaproszenia", + "info.messages.loading.older": "Ładowanie starszych wiadomości...", + "error:message.not.found": "Nie znaleziono wiadomości", + "title.profile": "Profil", + "title.settings": "Ustawienia", + "title.logout": "Wyloguj", + "title.loading": "Ładowanie...", + "title.details": "Szczegóły", + "title.members": "Członkowie", + "action.edit.profile": "Edytuj profil", + "action.open.dm": "Otwórz rozmowę", + "action.invite.accept": "Akceptuj zaproszenie", + "action.invite.cancel": "Anuluj zaproszenie", + "action.invite.send": "Zaproś do rozmowy", + "action.profile.loading": "Ładowanie profilu...", + "error.profile.load.failed": "Nie udało się załadować profilu.", + "unknown.author": "Ktoś", + "title.account": "Konto", + "title.delete.account": "Usuń konto", + "desc.delete.account": "Po usunięciu konta nie będzie możliwości jego odzyskania. Upewnij się, że na pewno chcesz to zrobić.", + "title.avatar": "Awatar", + "title.banner": "Baner", + "title.bio": "Opis", + "title.appearance": "Wygląd", + "title.language": "Język", + "action.save.profile": "Zapisz profil", + "action.save.bio": "Zapisz opis", + "action.uploading": "Przesyłanie...", + "action.bio.saving": "Zapisywanie opisu...", + "action.account.saving": "Zapisywanie konta...", + "action.account.deleting": "Usuwanie konta...", + "profile.updated": "Profil został pomyślnie zaktualizowany", + "avatar.updated": "Awatar został pomyślnie zaktualizowany", + "banner.updated": "Baner został pomyślnie zaktualizowany", + "action.save": "Zapisz", + "action.delete.account": "Usuń konto", + "account.deleted.scheduled": "Konto zostało oznaczone do usunięcia. Zostanie trwale usunięte po miesiącu braku aktywności. Zalogowanie się w tym czasie anuluje proces usuwania.", + "action.attachment.take.photo": "Zrób zdjęcie", + "action.attachment.media": "Multimedia", + "action.attachment.file": "Plik", + "action.attachment.poll": "Ankieta", + "action.attachment.uploading": "Przesyłanie załączników...", + "attachment.legacy": "Starszy załącznik", + "warning.unencrypted": "Ta wiadomość nie jest szyfrowana", + "action.change.avatar": "Zmień awatar", + "action.change.banner": "Zmień baner", + "placeholder.type.text.emoji": "Wpisz tekst / emoji...", + "action.cancel": "Anuluj", + "title.crop.image": "Przytnij obraz", + "title.zoom": "Przybliżenie", + "title.pin.message": "Przypnij", + "title.unpin.message": "Odepnij", + "message.pinned.by": "{0} przypiął {1}.", + "message.unpinned.by": "{0} odpiął {1}.", + "title.pinned.messages": "Przypięte", + "error.pinned.messages.empty": "Brak przypiętych wiadomości", + "title.message.lowercase": "wiadomość", + "action.rotate.keys": "Rotuj Klucze", + "already.in.group": "Już w grupie", + "desc.key.rotation.info": "Musisz zrotować klucze szyfrujące, aby kontynuować wysyłanie wiadomości w tej grupie. Inni członkowie automatycznie otrzymają nowy klucz.", + "desc.no.groups": "Nie jesteś jeszcze w żadnej grupie.", + "dm.read": "Przeczytano", + "empty.body": "Pusta zawartość", + "failed.create.group": "Nie udało się utworzyć grupy", + "federation.not.implemented": "Federacja niezaimplementowana", + "group.not.found": "Nie znaleziono grupy", + "group.read": "Przeczytano", + "invalid.entry.name": "Nieprawidłowa nazwa wpisu", + "invalid.key.index": "Nieprawidłowy indeks klucza (odśwież stronę)", + "invalid.msg.id": "Nieprawidłowe ID wiadomości", + "invalid.payload": "Nieprawidłowy ładunek", + "invalid.reaction.id": "Nieprawidłowe ID reakcji", + "invalid.user": "Nieprawidłowy użytkownik", + "joined": "Dołączono", + "key.rotation.required": "Wymagana rotacja kluczy", + "key.too.large": "Klucz zbyt duży", + "keys.public.mismatch": "Niezgodność kluczy publicznych", + "label.members": "CZŁONKOWIE", + "message.deleted": "Wiadomość usunięta", + "message.not.found": "Nie znaleziono wiadomości", + "message.pin.toggled": "Zmieniono przypięcie wiadomości", + "message.reacted": "Dodano reakcję", + "message.reaction.removed": "Reakcja usunięta", + "no.invite": "Brak zaproszenia", + "not.a.member": "Nie jesteś członkiem", + "not.in.group": "Nie jesteś w grupie", + "reaction.not.found": "Nie znaleziono reakcji", + "rotation.not.required": "Rotacja niewymagana", + "storage.entry.updated": "Zaktualizowano wpis pamięci", + "storage.limit.exceeded": "Przekroczono limit pamięci", + "title.key.rotation": "Rotacja kluczy", + "missing.keys": "Brakujące klucze", + "error:messages.decrypt.failed": "Deszyfrowanie nie powiodło się", + "error.message.already.deleted": "Wiadomość została już usunięta", + "group.user.joined.by": "{0} dołączył do grupy", + "group.created.by": "Grupa utworzona przez {0}", + "group.user.left.by": "{0} opuścił grupę", + "action.group.fetch": "Pobieranie grup", + "group.created": "Utworzono grupę", + "group.user.joined": "Dołączono do grupy", + "prompt.invite.username": "Podaj nazwę użytkownika, aby zaprosić:", + "desc.failed.load.members": "Nie udało się załadować członków", + "prompt.are.you.sure": "Czy na pewno?", + "title.unknown.group": "Nieznana grupa", + "title.unknown": "Nieznany", + "key.rotated": "Klucze grupy zostały pomyślnie zrotowane.", + "success:banner.updated": "Baner został pomyślnie zaktualizowany", + "success:profile.updated": "Profil został pomyślnie zaktualizowany", + "success:avatar.updated": "Awatar został pomyślnie zaktualizowany", + "en-us": "Angielski", + "pl-pl": "Polski", + "en-cat": "Koci (Meowlish)" +} diff --git a/android/app/src/main/assets/public/index.html b/android/app/src/main/assets/public/index.html index e6361f63..c7570624 100644 --- a/android/app/src/main/assets/public/index.html +++ b/android/app/src/main/assets/public/index.html @@ -119,6 +119,25 @@ + + @@ -208,8 +227,59 @@ } } - async function createGroup() { + async function refreshGroups(waittime = 0, showLoading = true) { + try { + if (showLoading) showAction("action.group.fetch", "grouprefresh"); + await delay(waittime); + if (window.cachedGroups && typeof renderGroups === 'function') { + await renderGroups(window.cachedGroups); + } + let res = await fetchEncrypted("user/group/list"); + window.cachedGroups = res; + if (typeof renderGroups === 'function') { + await renderGroups(res); + } + if (showLoading) clearAction("grouprefresh"); + } + catch (e) { + if (showLoading) clearAction("grouprefresh"); + showBlahNotification("error:group.refresh.failed"); + } + } + async function createGroup() { + try { + let groupName = document.getElementById("creategroup-name").value; + if (!groupName || groupName.trim() === "") { + showBlahNotification("error:group.name.empty"); + return; + } + + showAction("action.group.creating", "group.action"); + + await ensureUserKeys(); + const roomKey = randomRoomKey256Chars(); + const selfWrapped = await wrapRoomKeyForSelf(roomKey); + + let payload = JSON.stringify({ + string1: groupName, + string2: selfWrapped + }); + + let res = await fetchEncrypted("group/create", payload); + clearAction("group.action"); + + if (res.startsWith("success:")) { + showBlahNotification("success:group.created"); + await refreshGroups(200, false); + } else { + showBlahNotification(res || "error:something.wrong"); + } + } catch (e) { + clearAction("group.action"); + showBlahNotification("error:something.wrong"); + console.error(e); + } } diff --git a/android/app/src/main/assets/public/main.js b/android/app/src/main/assets/public/main.js index 1ae4b4b8..82922c97 100644 --- a/android/app/src/main/assets/public/main.js +++ b/android/app/src/main/assets/public/main.js @@ -367,6 +367,171 @@ async function ensureDmRoomKey(dmId) { window.ensureDmRoomKey = ensureDmRoomKey; +window.groupKeysCache = {}; + +async function getGroupKey(groupId, keyIndex) { + if (!window.groupKeysCache[groupId]) { + window.groupKeysCache[groupId] = {}; + } + if (window.groupKeysCache[groupId][keyIndex]) { + return window.groupKeysCache[groupId][keyIndex]; + } + + let payload = JSON.stringify({string1: groupId, string2: keyIndex.toString()}); + let res = await fetchEncrypted("group/key/get", payload); + if (!res || res.startsWith("error:")) { + return null; + } + + try { + let decryptedKey; + if (res.startsWith("SETUP:")) { + const rest = res.substring("SETUP:".length); + const parts = rest.split(";"); + if (parts.length < 2) throw new Error("error:group.key.setup.malformed"); + + const otherPublicJson = parts[0]; + const setupPayloadJson = parts.slice(1).join(";"); + const otherPublic = JSON.parse(otherPublicJson); + const setupPayload = JSON.parse(setupPayloadJson); + + const aesKeyBytes = await deriveHybridKeyFromSetup(otherPublic, setupPayload.ctMlKem); + decryptedKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes); + } else { + decryptedKey = await unwrapRoomKeyForSelf(res); + } + + window.groupKeysCache[groupId][keyIndex] = decryptedKey; + return decryptedKey; + } catch(e) { + return null; + } +} + +function closeRotationPopup() { + let popupContainer = document.getElementById("rotation-popup-container"); + if(popupContainer) { + popupContainer.classList.remove("show"); + setTimeout(() => { + popupContainer.style.display = "none"; + }, 200); + } + clearAction("key_rotation"); +} + +let pendingRotationMessagePayload = null; + +async function showRotationPopup(groupId, originalMessagePayload) { + pendingRotationMessagePayload = originalMessagePayload; + showAction("action.key.rotation", "key_rotation"); + + try { + let membersRaw = await fetchEncrypted("group/members", groupId); + if(membersRaw && membersRaw.startsWith("error:")) throw new Error(membersRaw); + + let members = JSON.parse(membersRaw); + + let desc = document.getElementById("rotation-popup-desc"); + desc.innerHTML = `desc.key.rotation.info

${members.length} label.members`; + let blahTags = desc.getElementsByTagName("blah"); + for (let i = 0; i < blahTags.length; i++) { + blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML); + } + + let popupContainer = document.getElementById("rotation-popup-container"); + popupContainer.style.display = "flex"; + popupContainer.offsetHeight; // trigger reflow + popupContainer.classList.add("show"); + } catch(e) { + clearAction("key_rotation"); + showBlahNotification("error:something.wrong"); + } +} + +async function executeKeyRotation() { + closeRotationPopup(); + showAction("action.key.rotating", "key_rotation_exec"); + + try { + let groupId = currentDmId; + + let membersRaw = await fetchEncrypted("group/members", groupId); + if(membersRaw && membersRaw.startsWith("error:")) throw new Error(membersRaw); + let members = JSON.parse(membersRaw); + + let roomKey = randomRoomKey256Chars(); + let keysDict = {}; + let myPublicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${window.id}`); + + for(let memberId of members) { + let pureId = memberId.split(";")[0]; + if(pureId === window.id) { + keysDict[memberId] = await wrapRoomKeyForSelf(roomKey); + } else { + let publicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${pureId}`); + if(publicKeysRaw && publicKeysRaw.length > 10) { + let publicKeys = JSON.parse(publicKeysRaw); + let hybrid = await deriveHybridKeyToUser(publicKeys); + let encForMember = await encryptAesGcmToBase64(roomKey, hybrid.aesKeyBytes); + let setupPayloadJson = JSON.stringify({ + v: 1, + alg: "hybrid-x25519-mlkem768", + ctMlKem: hybrid.ciphertextMlKemBase64, + enc: encForMember + }); + keysDict[memberId] = `SETUP:${myPublicKeysRaw};${setupPayloadJson}`; + } else { + throw new Error("error:missing.keys"); + } + } + } + + let payload = JSON.stringify({ + groupId: groupId, + keys: keysDict + }); + + let res = await fetchEncrypted("group/key/rotate", payload); + if(res && res.startsWith("error:")) { + throw new Error(res); + } + + // Success! + let newIndex = res.replace("success:", ""); + window.currentGroupKeyIndex = newIndex; + + if(!window.groupKeysCache[groupId]) window.groupKeysCache[groupId] = {}; + window.groupKeysCache[groupId][newIndex] = roomKey; + + clearAction("key_rotation_exec"); + showBlahNotification("success:key.rotated"); + + // Retry sending the message! + if(pendingRotationMessagePayload) { + let p = pendingRotationMessagePayload; + p.string5 = newIndex; + let dmKeyBytes = await sha256Bytes(base64ToUint8(roomKey)); + p.string2 = await encryptAesGcmToBase64(p.rawPlainContent, dmKeyBytes); // we need to save rawPlainContent for retry + + showAction("action.message.sending", "msgsending"); + let resSend = await fetchEncrypted("group/message/send", JSON.stringify(p)); + clearAction("msgsending"); + + if(resSend && resSend.startsWith("error:")) { + showBlahNotification(resSend); + } else if(resSend && resSend.startsWith("success:")) { + loadDmMessages(currentDmId); + } + } + pendingRotationMessagePayload = null; + + } catch(e) { + clearAction("key_rotation_exec"); + console.error(e); + showBlahNotification(e.message && e.message.startsWith("error:") ? e.message : "error:something.wrong"); + } +} + async function encrypt(plainText, keyBytes) { const iv = window.crypto.getRandomValues(new Uint8Array(16)); const encoder = new TextEncoder(); @@ -857,6 +1022,18 @@ async function getAvatarUrl(id, username) { } } +async function getGroupAvatarUrl(groupId, groupName) { + try { + let pfpUrl = `${url}/group/storage/public/getentry?id=${groupId}&e=larp.profile.pfp`; + if ((await fetchAsync(pfpUrl)) == "") { + throw Error(); + } + return pfpUrl; + } catch (e) { + return createAvatarSvg(groupName); + } +} + function updateLoadingStatus(message) { loadingStatus.innerHTML = processBlah(message); } @@ -1743,6 +1920,71 @@ function setActiveRoombarItem(itemId) { } } +async function acceptGroupInvite(groupId, inviterId) { + try { + showAction("action.invite.accepting", "invite.action"); + + let payload = JSON.stringify({ + string1: groupId, + string2: inviterId + }); + + let res = await fetchEncrypted("group/join", payload); + clearAction("invite.action"); + showBlahNotification(res || "success:joined"); + + // Refresh invites and groups + window.cachedInvites = {'received': null, 'sent': null}; + loadInvites("received"); + if (typeof refreshGroups === 'function') refreshGroups(0, false); + } catch (e) { + clearAction("invite.action"); + showBlahNotification("error:something.wrong"); + } +} + +async function declineGroupInvite(groupId, inviterId) { + try { + showAction("action.invite.declining", "invite.action"); + + let payload = JSON.stringify({ + string1: groupId, + string2: inviterId + }); + + let res = await fetchEncrypted("group/invite/decline", payload); + clearAction("invite.action"); + showBlahNotification(res || "success:invite.declined"); + + window.cachedInvites = {'received': null, 'sent': null}; + loadInvites("received"); + } catch (e) { + clearAction("invite.action"); + showBlahNotification("error:something.wrong"); + } +} + +async function revokeGroupInvite(groupId, targetId) { + try { + showAction("action.invite.revoking", "invite.action"); + + let payload = JSON.stringify({ + string1: groupId, + string2: targetId + }); + + let res = await fetchEncrypted("group/invite/revoke", payload); + clearAction("invite.action"); + showBlahNotification(res || "success:invite.revoked"); + + window.cachedInvites = {'received': null, 'sent': null}; + loadInvites("sent"); + } catch (e) { + clearAction("invite.action"); + showBlahNotification("error:something.wrong"); + } +} + async function renderInvites(res, type) { let container = document.getElementById("invites-container"); if (!container) return; @@ -1781,7 +2023,7 @@ async function renderInvites(res, type) { } let onlyId = id.split(":")[0]; let username = await fetchAsync(`${url}/idtoname?id=${id}`); - if (!(username && username.trim() !== "")) return; + if (!(username && username.trim() !== "")) continue; count++; let pfp = await getAvatarUrl(id, username); @@ -1789,8 +2031,9 @@ async function renderInvites(res, type) { let actions = ""; let desc = ""; + let displayName = username.split(':')[0] || onlyId; if (type === "received") { - desc = `:desc.invite.dm.received:${username};${onlyId}`; + desc = `:desc.invite.dm.received:${displayName};${onlyId}`; actions = ` `; } else { - desc = `:desc.invite.dm.sent:${username};${onlyId}`; + desc = `:desc.invite.dm.sent:${displayName};${onlyId}`; actions = ` + + `; + } else { + desc = `:desc.invite.group.sent:${val1};${val2}`; + actions = ` + + `; + } + + let entry = invitesEntry + .replaceAll("{username}", groupName) + .replaceAll("{pfp}", pfp) + .replaceAll("{desc}", `${desc}`) + .replaceAll("{actions}", actions) + .replaceAll("{onclick_attr}", "") + .replaceAll("{id}", groupId); + html += entry; } } @@ -1996,7 +2297,10 @@ async function gotoHome(splash = true) { switchRoomsBar("title.home", homeRoomBar); setActiveRoombarItem(null); setupWebSocket(); - await refreshDms(210); + await Promise.all([ + refreshDms(210), + (typeof refreshGroups === 'function' ? refreshGroups(210) : Promise.resolve()) + ]); } function setActiveSidebarIndicator(element, isTemporary = false) { @@ -2210,6 +2514,67 @@ async function renderDms(res) { } } +async function renderGroups(res) { + let container = document.getElementById("groups-list"); + if (!container) return; + + if (res && res !== "") { + let parsed = JSON.parse(res); + let groups = parsed.groups || {}; + let groupEntries = Object.entries(groups); + + //Sort by timestamp descending + groupEntries.sort((a, b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0)); + + let html = ""; + let count = 0; + + for (let [groupId, groupData] of groupEntries) { + let isRead = groupData.string3 === "true"; + let nameStyle = isRead ? "opacity: 0.8;" : "opacity: 1;"; + + let infoRes = await fetchEncrypted("group/info", groupId); + if (infoRes && infoRes.startsWith("{")) { + let info = JSON.parse(infoRes); + let groupName = info.string2; + + let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27"); + let pfp = await getGroupAvatarUrl(groupId, groupName); + + html += ` + + `; + count++; + } + } + + if (count > 0) { + container.innerHTML = html; + container.classList.remove("empty"); + } else { + container.innerHTML = `

desc.no.groups

`; + container.classList.add("empty"); + let blahTags = container.getElementsByTagName("blah"); + for (let i = 0; i < blahTags.length; i++) { + blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML); + } + } + } else { + container.innerHTML = `

desc.no.groups

`; + container.classList.add("empty"); + let blahTags = container.getElementsByTagName("blah"); + for (let i = 0; i < blahTags.length; i++) { + blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML); + } + } +} + var currentDmId = null; var currentDmKey = null; // AES key for encrypting/decrypting messages var dmUsernameCache = {}; @@ -2263,30 +2628,131 @@ async function generateDmMembersHtml(targetId, targetUsername) { `; } -async function openDm(dmId, username, targetId) { +async function inviteToGroupPrompt(groupId) { + let targetUsername = prompt(processBlah("prompt.invite.username")); + if (!targetUsername) return; + + if (!targetUsername.includes(":")) { + targetUsername += `:${host}`; + } + + let idRes = await fetchAsync(`${url}/nametoid?u=${targetUsername}`); + if (idRes.startsWith("error") || idRes.trim() === "0" || idRes.trim() === "") { + showBlahNotification("error:user.not.found"); + return; + } + let targetId = idRes.trim(); + + try { + showAction("action.invite.sending", "invite.action"); + + let payload = JSON.stringify({ + string1: groupId, + string2: targetId + }); + + let res = await fetchEncrypted("group/invite", payload); + clearAction("invite.action"); + showBlahNotification(res || "success:invite.sent"); + } catch (e) { + clearAction("invite.action"); + showBlahNotification("error:something.wrong"); + } +} + +async function generateGroupMembersHtml(groupId, groupName) { + let membersJson = await fetchEncrypted("group/members", groupId); + if (!membersJson || membersJson.startsWith("error")) { + return `
desc.failed.load.members
`; + } + + let members = JSON.parse(membersJson); + let html = ` +
+ `; + + for (let memberId of members) { + let memberUsername = await fetchAsync(`${url}/idtoname?id=${memberId}`); + if (!memberUsername || memberUsername.startsWith("error")) continue; + + let memberPfp = await getAvatarUrl(memberId, memberUsername); + let memberDisplayName = memberUsername.split(':')[0]; + + html += ` + `; + } + + html += `
`; + return html; +} + +async function openDm(dmId, username, targetId, type = 'dms') { try { showAction("action.dm.opening", "dmopen"); - markDmAsRead(dmId); + if (type === 'groups') { + markDmAsRead(dmId, 'groups'); + } else { + markDmAsRead(dmId, 'dms'); + } - currentDmKey = await ensureDmRoomKey(dmId); - if (!currentDmKey) { - throw new Error("Missing DM room key"); + if (type === 'dms') { + currentDmKey = await ensureDmRoomKey(dmId); + if (!currentDmKey) { + throw new Error("Missing DM room key"); + } + } else { + let infoPayload = await fetchEncrypted("group/info", dmId); + if (infoPayload && !infoPayload.startsWith("error:")) { + let info = JSON.parse(infoPayload); + window.currentGroupKeyIndex = info.string4 || "0"; + } else { + window.currentGroupKeyIndex = "0"; + } + } + + if (currentDmId !== dmId) { + currentRoomContentTitle = ""; } currentDmId = dmId; + window.currentRoomType = type; // Save the type globally loadedMessages = {}; oldestLoadedMsgId = null; isLoadingOlderMessages = false; - let pfp = await getAvatarUrl(targetId, username); - let iconHtml = ``; + let iconHtml = ""; + if (type === 'dms') { + let pfp = await getAvatarUrl(targetId, username); + iconHtml = ``; + } else { + let pfp = await getGroupAvatarUrl(dmId, username); + iconHtml = ``; + } if (currentRoomsBarTitle !== "title.home") { gotoHome(false); } await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml); - let membersHtml = await generateDmMembersHtml(targetId, username); + + let membersHtml = ""; + if (type === 'dms') { + membersHtml = await generateDmMembersHtml(targetId, username); + } else { + membersHtml = await generateGroupMembersHtml(dmId, username); + } await switchDetailsContent("title.details", membersHtml); let msgContainer = document.getElementById("chat-messages"); @@ -2326,7 +2792,8 @@ async function openDm(dmId, username, targetId) { async function loadDmMessages(dmId, startOffset = "", isPrepend = false) { try { let payload = JSON.stringify({string1: dmId, string2: "50", string3: startOffset}); - let res = await fetchEncrypted("dm/messages/get", payload); + let endpoint = window.currentRoomType === 'groups' ? "group/messages/get" : "dm/messages/get"; + let res = await fetchEncrypted(endpoint, payload); if (res && res.startsWith("error:")) { showBlahNotification(res); @@ -2389,9 +2856,26 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { let decrypted = false; if (msg.key && msg.key !== "") { try { - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); - content = await decryptAesGcmFromBase64(content, dmKeyBytes); - decrypted = true; + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + keyToUse = await getGroupKey(currentDmId, msg.key); + if (keyToUse) { + let mK = parseInt(msg.key); + let cK = parseInt(window.currentGroupKeyIndex || "0"); + if (!isNaN(mK) && !isNaN(cK) && mK > cK) { + window.currentGroupKeyIndex = msg.key; + } + } + } + + if (!keyToUse) { + content = `error:messages.decrypt.failed`; + decrypted = false; + } else { + let keyBytes = await sha256Bytes(base64ToUint8(keyToUse)); + content = await decryptAesGcmFromBase64(content, keyBytes); + decrypted = true; + } } catch (e) { content = `error:messages.decrypt.failed`; decrypted = false; @@ -2423,7 +2907,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { let safeType = encodeURIComponent(att.type).replace(/'/g, "%27"); let safeName = encodeURIComponent(att.name).replace(/'/g, "%27"); - attachmentsHtml += `
+ attachmentsHtml += `
${iconSvg}
${escapeHtml(displayName)}
@@ -2454,8 +2938,8 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { content = processBlah(content); if (msg.type && msg.type.startsWith("larp.info")) { - if (msg.type === "larp.info.pin" || msg.type === "larp.info.unpin") { - let authorName = "Unknown"; + if (msg.type === "larp.info.pin" || msg.type === "larp.info.unpin" || msg.type === "larp.info.join" || msg.type === "larp.info.leave" || msg.type === "larp.info.create") { + let authorName = processBlah("title.unknown"); if (msg.author && msg.author !== "0") { if (!dmUsernameCache[msg.author]) { let fullUsername = await fetchAsync(`${url}/idtoname?id=${msg.author}`); @@ -2471,14 +2955,22 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { } } - let isPinned = msg.type === "larp.info.pin"; - let actionStr = isPinned ? "message.pinned.by" : "message.unpinned.by"; + let actionStr = ""; + if (msg.type === "larp.info.pin") actionStr = "message.pinned.by"; + else if (msg.type === "larp.info.unpin") actionStr = "message.unpinned.by"; + else if (msg.type === "larp.info.join") actionStr = "group.user.joined.by"; + else if (msg.type === "larp.info.leave") actionStr = "group.user.left.by"; + else if (msg.type === "larp.info.create") actionStr = "group.created.by"; - let clickableMsg = `${processBlah('title.message.lowercase')}`; let clickableAuthor = `${authorName}`; let textStr = processBlah(actionStr); - content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg); + if (msg.type.endsWith(".pin") || msg.type.endsWith(".unpin")) { + let clickableMsg = `${processBlah('title.message.lowercase')}`; + content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg); + } else { + content = textStr.replace("{0}", clickableAuthor); + } } html += `
${content}
`; lastAuthor = null; @@ -2501,7 +2993,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { } lastAuthor = msg.author; - let authorName = "Unknown"; + let authorName = processBlah("title.unknown"); let pfp = "assets/default_avatar.png"; if (msg.author !== "0") { if (!dmUsernameCache[msg.author]) { @@ -2618,8 +3110,15 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { let rxVal = rx.r; if (msg.key && msg.key !== "") { try { - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); - rxVal = await decryptAesGcmFromBase64(rxVal, dmKeyBytes); + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + let k = await getGroupKey(currentDmId, msg.key); + if (k) keyToUse = k; + } + if (keyToUse) { + let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse)); + rxVal = await decryptAesGcmFromBase64(rxVal, dmKeyBytes); + } } catch(e) {} } if (!rxCounts[rxVal]) rxCounts[rxVal] = {count: 0, me: false, myReactionId: null}; @@ -2839,8 +3338,8 @@ function setupChatScrollListener() { -async function downloadAndRenderAttachment(container, msgId, attId, type, name) { - if (!currentDmId || !currentDmKey) return; +async function downloadAndRenderAttachment(container, msgId, attId, type, name, keyIndex = "0") { + if (!currentDmId || (window.currentRoomType === 'dms' && !currentDmKey)) return; if (!container) return; if (container.dataset.loaded === "true") { @@ -2860,10 +3359,16 @@ async function downloadAndRenderAttachment(container, msgId, attId, type, name) container.innerHTML = `
${ICONS.spinner()}
`; try { - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + let k = await getGroupKey(currentDmId, keyIndex); + if(k) keyToUse = k; + } + let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse)); let nonce = await getNonce(id, passwordHash); - let res = await fetch(`${url}/dm/attachment/get?id=${id}&dm=${currentDmId}&msg=${msgId}&att=${attId}`, { + let endpointType = window.currentRoomType === 'groups' ? "group" : "dm"; + let res = await fetch(`${url}/${endpointType}/attachment/get?id=${id}&dm=${currentDmId}&msg=${msgId}&att=${attId}`, { headers: { "secret": await encryptWithNonce(passwordHash, passwordHash, nonce) } @@ -2953,7 +3458,7 @@ function renderAttachmentsPreview() { } async function sendMessage(sendbutton = document.getElementById("chat-sendmessage")) { - if (!currentDmId || !currentDmKey) return; + if (!currentDmId || (window.currentRoomType === 'dms' && !currentDmKey)) return; let input = document.getElementById("chat-input"); let content = input.value.trim(); @@ -2975,7 +3480,13 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag } try { - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + let k = await getGroupKey(currentDmId, window.currentGroupKeyIndex || "0"); + if(k) keyToUse = k; + } + + let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse)); let encryptedContent = await encryptAesGcmToBase64(content, dmKeyBytes); let attachmentsPayload = []; @@ -3035,6 +3546,11 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag string3: attachmentsPayload.length > 0 ? JSON.stringify(attachmentsPayload) : "", string4: replyingToMsgId || "" }; + + if (window.currentRoomType === 'groups') { + msgPayload.string5 = window.currentGroupKeyIndex || "0"; + msgPayload.rawPlainContent = content; // for retry + } input.value = ""; input.style.height = '2.5rem'; @@ -3049,9 +3565,14 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag showAction("action.message.sending", "msgsending"); - let res = await fetchEncrypted("dm/message/send", JSON.stringify(msgPayload)); + let res = await fetchEncrypted(window.currentRoomType === 'groups' ? "group/message/send" : "dm/message/send", JSON.stringify(msgPayload)); clearAction("msgsending"); - if (res && res.startsWith("error:")) { + + if (res && res.startsWith("error:key.rotation.required") && window.currentRoomType === 'groups') { + let pElem = document.getElementById(pendingMsgId); + if (pElem) pElem.remove(); + showRotationPopup(currentDmId, msgPayload); + } else if (res && res.startsWith("error:")) { showBlahNotification(res); let pElem = document.getElementById(pendingMsgId); if (pElem) pElem.remove(); @@ -3073,7 +3594,7 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag type: "larp.text", content: encryptedContent, attachment: finalAttPayload, - key: "0", + key: window.currentRoomType === 'groups' ? (window.currentGroupKeyIndex || "0") : "0", pervious: "", responded: replyingToMsgId || "", reactions: "" @@ -3148,15 +3669,16 @@ function setupWebSocket() { appWebSocket.onmessage = (event) => { let data = event.data; - if (data.startsWith("dm_message:")) { + if (data.startsWith("dm_message:") || data.startsWith("group_message:")) { let parts = data.split(":"); + let msgTypeRoom = data.startsWith("dm_message:") ? "dms" : "groups"; let msgDmId = parts[1]; let msgType = parts[2] || "unread"; if (msgType === "unread") { - updateDmListUI(msgDmId, false, Date.now().toString()); + updateDmListUI(msgDmId, false, msgTypeRoom, Date.now().toString()); } else if (msgType === "read") { - updateDmListUI(msgDmId, true, Date.now().toString()); + updateDmListUI(msgDmId, true, msgTypeRoom, Date.now().toString()); } else if (msgType === "update") { // do nothing to UI sorting/read state } @@ -3246,7 +3768,8 @@ async function togglePinMessage(msgId) { string1: currentDmId, string2: msgId }); - await fetchEncrypted("dm/message/pin/toggle", payload); + let endpoint = window.currentRoomType === 'groups' ? "group/message/pin/toggle" : "dm/message/pin/toggle"; + await fetchEncrypted(endpoint, payload); } catch (e) { console.error(e); showBlahNotification("error.network.error"); @@ -3270,7 +3793,8 @@ async function deleteMessage(msgId) { string1: currentDmId, string2: msgId }; - let res = await fetchEncrypted("dm/message/delete", JSON.stringify(msgPayload)); + let endpoint = window.currentRoomType === 'groups' ? "group/message/delete" : "dm/message/delete"; + let res = await fetchEncrypted(endpoint, JSON.stringify(msgPayload)); if (res && res.startsWith("error:")) { showBlahNotification(res); } @@ -3346,8 +3870,15 @@ async function replyMessage(msgId) { let content = msg ? msg.content : ""; if (msg && msg.key && msg.key !== "") { try { - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); - content = await decryptAesGcmFromBase64(content, dmKeyBytes); + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + let k = await getGroupKey(currentDmId, msg.key); + if(k) keyToUse = k; + } + if (keyToUse) { + let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse)); + content = await decryptAesGcmFromBase64(content, dmKeyBytes); + } } catch (e) { } } @@ -3454,8 +3985,15 @@ async function reactMessagePrompt(msgId, quickReaction = null) { let rxVal = rx.r; if (currentMsg.key && currentMsg.key !== "") { try { - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); - rxVal = await decryptAesGcmFromBase64(rxVal, dmKeyBytes); + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + let k = await getGroupKey(currentDmId, currentMsg.key); + if(k) keyToUse = k; + } + if (keyToUse) { + let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse)); + rxVal = await decryptAesGcmFromBase64(rxVal, dmKeyBytes); + } } catch(e) {} } if (rx.u === id && rxVal === reaction) { @@ -3468,10 +4006,17 @@ async function reactMessagePrompt(msgId, quickReaction = null) { } let isAdding = (myReactionId === null); - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + let k = await getGroupKey(currentDmId, currentMsg.key); + if(k) keyToUse = k; + } + let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse)); let encryptedReaction = await encryptAesGcmToBase64(reaction, dmKeyBytes); - let endpoint = isAdding ? "dm/message/react/add" : "dm/message/react/remove"; + let endpoint = window.currentRoomType === 'groups' + ? (isAdding ? "group/message/react/add" : "group/message/react/remove") + : (isAdding ? "dm/message/react/add" : "dm/message/react/remove"); let msgPayload = { string1: currentDmId, string2: msgId, @@ -3615,7 +4160,7 @@ async function changePassword() { } async function deleteAccount() { - if (!confirm(processBlah("desc.delete.account") + "\n\nAre you sure?")) return; + if (!confirm(processBlah("desc.delete.account") + "\n\n" + processBlah("prompt.are.you.sure"))) return; showAction("action.account.deleting", "accountdel"); let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, ""); clearAction("accountdel"); @@ -3978,48 +4523,87 @@ if (container) { document.addEventListener('touchend', endDrag); } -function updateDmListUI(dmId, isRead, timestamp = null) { +function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) { try { - if (!window.cachedDms) return; - let parsedDms = JSON.parse(window.cachedDms); - if (!parsedDms.dms || !parsedDms.dms[dmId]) return; - - let changed = false; - if (parsedDms.dms[dmId].string3 !== (isRead ? "true" : "false")) { - parsedDms.dms[dmId].string3 = isRead ? "true" : "false"; - changed = true; - } - if (timestamp && parsedDms.dms[dmId].string2 !== timestamp) { - parsedDms.dms[dmId].string2 = timestamp; - changed = true; - } - - if (changed) { - window.cachedDms = JSON.stringify(parsedDms); - } - - let btn = document.getElementById(`dm-btn-${dmId}`); - let container = document.getElementById("dms-list"); - if (btn && container) { - let nameSpan = btn.querySelector('.room-name'); - if (nameSpan) { - nameSpan.style.opacity = isRead ? "0.8" : "1"; + if (type === 'groups') { + if (!window.cachedGroups) return; + let parsedGroups = JSON.parse(window.cachedGroups); + if (!parsedGroups.groups || !parsedGroups.groups[roomId]) return; + + let changed = false; + if (parsedGroups.groups[roomId].string3 !== (isRead ? "true" : "false")) { + parsedGroups.groups[roomId].string3 = isRead ? "true" : "false"; + changed = true; } - if (timestamp) { - container.prepend(btn); + if (timestamp && parsedGroups.groups[roomId].string2 !== timestamp) { + parsedGroups.groups[roomId].string2 = timestamp; + changed = true; + } + + if (changed) { + window.cachedGroups = JSON.stringify(parsedGroups); + } + + let btn = document.getElementById(`group-btn-${roomId}`); + let container = document.getElementById("groups-list"); + if (btn && container) { + let nameSpan = btn.querySelector('.room-name'); + if (nameSpan) { + nameSpan.style.opacity = isRead ? "0.8" : "1"; + } + if (timestamp) { + container.prepend(btn); + } + } else { + if (typeof refreshGroups === 'function') refreshGroups(0, false); } } else { - if (typeof refreshDms === 'function') refreshDms(0, false); + if (!window.cachedDms) return; + let parsedDms = JSON.parse(window.cachedDms); + if (!parsedDms.dms || !parsedDms.dms[roomId]) return; + + let changed = false; + if (parsedDms.dms[roomId].string3 !== (isRead ? "true" : "false")) { + parsedDms.dms[roomId].string3 = isRead ? "true" : "false"; + changed = true; + } + if (timestamp && parsedDms.dms[roomId].string2 !== timestamp) { + parsedDms.dms[roomId].string2 = timestamp; + changed = true; + } + + if (changed) { + window.cachedDms = JSON.stringify(parsedDms); + } + + let btn = document.getElementById(`dm-btn-${roomId}`); + let container = document.getElementById("dms-list"); + if (btn && container) { + let nameSpan = btn.querySelector('.room-name'); + if (nameSpan) { + nameSpan.style.opacity = isRead ? "0.8" : "1"; + } + if (timestamp) { + container.prepend(btn); + } + } else { + if (typeof refreshDms === 'function') refreshDms(0, false); + } } } catch (e) { console.error(e); if (typeof refreshDms === 'function') refreshDms(0, false); + if (typeof refreshGroups === 'function') refreshGroups(0, false); } } -function markDmAsRead(dmId) { - fetchEncrypted("dm/read", JSON.stringify({string1: dmId})).catch(()=>{}); - updateDmListUI(dmId, true); +function markDmAsRead(roomId, type = 'dms') { + if (type === 'groups') { + fetchEncrypted("group/read", JSON.stringify({string1: roomId})).catch(()=>{}); + } else { + fetchEncrypted("dm/read", JSON.stringify({string1: roomId})).catch(()=>{}); + } + updateDmListUI(roomId, true, type); } function handleDmInteraction() { @@ -4029,12 +4613,21 @@ function handleDmInteraction() { if (lastChatScroll <= 10) { try { - if (window.cachedDms) { - let parsedDms = JSON.parse(window.cachedDms); - if (parsedDms.dms && parsedDms.dms[currentDmId] && parsedDms.dms[currentDmId].string3 !== "true") { - markDmAsRead(currentDmId); + if (window.currentRoomType === 'groups') { + if (window.cachedGroups) { + let parsedGroups = JSON.parse(window.cachedGroups); + if (parsedGroups.groups && parsedGroups.groups[currentDmId] && parsedGroups.groups[currentDmId].string3 !== "true") { + markDmAsRead(currentDmId, window.currentRoomType); + } + } + } else { + if (window.cachedDms) { + let parsedDms = JSON.parse(window.cachedDms); + if (parsedDms.dms && parsedDms.dms[currentDmId] && parsedDms.dms[currentDmId].string3 !== "true") { + markDmAsRead(currentDmId, window.currentRoomType); + } + } } - } } catch (e) {} } } @@ -4056,7 +4649,8 @@ async function showPinnedMessages(forceReload = false, showPanel = true) { } try { - let res = await fetchEncrypted("dm/messages/pinned", currentDmId); + let endpoint = window.currentRoomType === 'groups' ? "group/messages/pinned" : "dm/messages/pinned"; + let res = await fetchEncrypted(endpoint, currentDmId); if (res && !res.startsWith("error:")) { let msgs = JSON.parse(res); let entries = Object.entries(msgs).sort((a, b) => parseInt(a[0]) - parseInt(b[0])); diff --git a/android/app/src/main/assets/public/screens.js b/android/app/src/main/assets/public/screens.js index 84e3d765..ead665d2 100644 --- a/android/app/src/main/assets/public/screens.js +++ b/android/app/src/main/assets/public/screens.js @@ -117,10 +117,10 @@ var notifisScreen = ` var invitesEntry = `
- +
- {username} - {desc} + {username} + {desc}
diff --git a/android/app/src/main/assets/public/style.css b/android/app/src/main/assets/public/style.css index 84660d6f..58e48f25 100644 --- a/android/app/src/main/assets/public/style.css +++ b/android/app/src/main/assets/public/style.css @@ -215,6 +215,25 @@ sidebar { width: calc(var(--icon-button-height) + (var(--button-margin) * 2) + var(--border-width)); border-right: var(--border-width) solid var(--light-border-color); } + +sidebar:not(.second) { + overflow-y: auto; + overflow-x: hidden; +} + +sidebar:not(.second)::-webkit-scrollbar { + display: none; +} +sidebar:not(.second) { + -ms-overflow-style: none; + scrollbar-width: none; +} + +#roomsbar roomcontent2 { + overflow-y: auto; + overflow-x: hidden; +} + sidebar.second { width: 17rem; min-width: 17rem; diff --git a/webroot/blah/en-cat.json b/webroot/blah/en-cat.json index e2d7812c..3d8cd6fa 100644 --- a/webroot/blah/en-cat.json +++ b/webroot/blah/en-cat.json @@ -1,205 +1,261 @@ { - "user.not.found": "meowser not found :c", - "dm.already.exists": "direct meowchat already exists :3", - "cant.invite.urself": "you can't invite yourself, silly :3", - "user.already.invited": "you have already invited this user meow", - "user.invited": "user invited successfully :3", - "cant.create.dm.without.invitation": "you can't create a dm without invitation ><", - "dm.begin.notice": "at the beginning of a meowchat, you should always verify that the person you're talking to is the intended recipient", - "failed.accept.dm": "failed to accept direct meowchat", - "dm.accepted": "direct meowchat accepted", - "no.invite.found": "you can't create a dm without invitation *hiss*", - "unknown.error": "unknown error :o", - "forbidden": "you do not have pawmission to perform this action *hiss*", - "dm.key.too.large": "meowchat key too large :o", - "dm.key.not.found": "meowchat key not found :c", - "dm.key.updated": "meowchat key updated successfully :3", - "body.too.large": "Meow is too big! Max meow is {0} MB.", - "message.too.long": "Meowssage exceeds maximum allowed size ({0}).", - "reaction.too.long": "Re-meow-ction exceeds maximum allowed size ({0}).", - "invite.revoked": "invite revoked successfully", - "invite.declined": "invite declined successfully", - "account.creation.request.expired": "cat creation request expired. try again", - "invalid.username": "invalid cat name: {0}", - "invalid.password": "invalid meow word: {0}", - "incorrect.captcha": "incorrect captcha. try meowgain", - "username.taken": "this cat name is already taken", - "accounts.slots.full": "you can't create a new cat because all collars are used. try again later", - "registration.disabled": "registration disabled", - "account.created": "cat created successfully", - "password.changed": "meow word changed successfully", - "keys.updated": "nametag updated successfully", - "unknown.request": "unknown request: {0}", - "login.successful": "pawgin successful :3", - "username.length": "cat name must be {0} characters long", - "username.conditions.allowed": "cat name can only include {all}", - "password.not.hashed.properly": "meow word is not hashed properly", - "invalid.username.or.password": "invalid cat name or meow word", - "error.storage.limit.exceeded": "Storage Limit Exceeded, purr", - "error.network.error": "Network error, check connection or file size, meow.", - "account.not.exist": "cat with this name doesn't exist", - "invalid.nonce": "invalid nonce. try again", - "username.changed": "cat name changed successfully", - "auth.failed.redirect.to.login": "failed to find owner. redirecting to pawgin...", - "dm.refresh.failed": "failed to refresh direct meowchats", - "chat.add.failed": "failed to add meowchat", - "something.wrong.mayb.pass": "something went wrong... :c (probably wrong meow word)", - "something.wrong": "something went wrong... :c", - "passwords.not.match": "meow word do not match", - "password.cant.empty": "meow word cannot be empty", - "username.cant.empty": "cat name cannot be empty", - "bad.request": "bad request!!!", - "letters": "letters", - "numbers": "numbers", - "underscores": "underscores", - "loading.connecting": "connecting...", - "loading.loading": "loading...", - "loading.done": "ready! :3", - "title.home": "bed", - "title.dms": "direct meowchats", - "title.groups": "clowder", - "title.welcome.splash": "welpaw to Meowven", - "desc.welcome.splash": "first Larpix client", - "title.splash": "splash", - "title.create.dm": "invite to direct meowchat", - "title.create.space": "create house", - "title.create.group": "create clowder", - "title.add.chat": "add meowchat", - "desc.add.chat": "add a private, encrypted meowchat by entering a cat name", - "desc.create.group": "create a private, encrypted clowder", - "placeholder.create.group.input": "my clowder", - "placeholder.create.space.input": "my house", - "title.name": "name", - "desc.create.space": "create a house for your community", - "desc.join.space": "join a community that fits you", - "title.join.space": "join house", - "title.space.id": "house id", - "title.inbox": "inbox", - "title.invites": "invites", - "title.notifications": "notifications", - "title.received": "received", - "title.sent": "sent", - "title.all": "all", - "title.unread": "unread", - "desc.no.invites": "no invites found :c", - "desc.no.notifications": "no notifications found :c", - "desc.fetching.invites": "fetching invites...", - "desc.fetching.notifications": "fetching notifications...", - "desc.invite.dm.received": "{0} ({1}) invited you to meowchat", - "desc.invite.dm.sent": "you invited {0} ({1}) to meowchat", - "desc.invite.group.received": "{0} ({1}) invited you to a clowder", - "desc.invite.group.sent": "you invited {0} ({1}) to a clowder", - "action.invite.sending": "Sending mewvite...", - "action.processing": "Purrcessing...", - "action.fetching.invites.sent": "Fetching sent mewvites...", - "action.fetching.invites.recv": "fetching received invites...", - "action.dm.fetch": "fetching direct meowchats...", - "action.auth": "authenticating...", - "action.dm.adding": "adding...", - "action.invite.revoking": "revoking invite...", - "action.invite.accepting": "accepting invite...", - "action.invite.declining": "declining invite...", - "title.sign.up": "sign up", - "title.sign.in": "sign in", - "title.sign.in.to": "sign in to your larpix instance", - "title.server.host": "server host", - "title.username": "meow name", - "title.password": "meow word", - "title.new.here.?": "new here?", - "title.create.an.account": "create a cat", - "join.larpix": "join Larpix", - "title.create.new.account": "create new larpix cat account", - "notice.use.strong.pass": "remember to use a strong meow word! tt will be used to lock your collar", - "title.confirm.password": "confirm meow word", - "title.already.registered.?": "already a kitty?", - "title.back.to.login": "back to pawgin", - "title.verify": "verify", - "title.captcha.desc": "prove you are a kitty", - "title.captcha.code": "captcha code", - "title.invitation.code": "invitation code", - "title.back.to.register": "back to registration", - "title.details": "details", - "title.members": "cats", - "placeholder.username": "cat name", - "placeholder.captcha.code": "captcha code", - "desc.messages.loading": "loading meows...", - "desc.no.dms": "no direct meowchats :c", - "action.dm.opening": "opening meowchat...", - "dm.open.failed": "failed to open meowchat :c", - "keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on", - "keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c", - "dm.messages.fetch.failed": "failed to fetch meows :c", - "messages.decrypt.failed": "failed to decrypt meow :c", - "title.reply.message": "meow back", - "title.react.message": "purr", - "title.text.react": "text purr", - "title.delete.message": "hiss away", - "title.replied.to": "meowed back to", - "title.replying.to": "meowing back to", - "action.message.deleting": "hissing message away...", - "action.message.sending": "sending meow...", - "action.message.reacting": "adding purr...", - "error:message.delete.failed": "failed to hiss away meow", - "error:message.send.failed": "failed to send meow", - "error:message.react.failed": "failed to add purr", - "info.sending.message": "sending...", - "placeholder.message.input": "meow...", - "larp.redacted": "Meow meow was meowed.", - "message.already.deleted": "This meow has already been meowed.", - "rate.limit": "You are meowing too quickly. Please meow again later.", - "placeholder.invitation.code": "meow meow meow", - "info.messages.loading.older": "Loading older meowsages...", - "error:message.not.found": "Meowsage not found", - "title.profile": "cat profile", - "title.settings": "cat settings", - "title.logout": "log out", - "title.loading": "sniffing...", - "action.edit.profile": "edit cat profile", - "action.open.dm": "open direct meowchat", - "action.invite.accept": "accept meow invite", - "action.invite.cancel": "hiss away invite", - "action.invite.send": "invite to meowchat", - "action.profile.loading": "sniffing profile...", - "error.profile.load.failed": "failed to sniff profile.", - "unknown.author": "somecat", - "title.account": "Cat", - "title.delete.account": "Delete cat", - "desc.delete.account": "Once you delete your cat, it will go to cat heaven. Please be certain :c", - "title.avatar": "Cat picture", - "title.banner": "Cat banner", - "title.bio": "Cat bio", - "title.appearance": "Appearance", - "title.language": "Meownguage", - "action.save.profile": "Save cat profile", - "action.save.bio": "Save meow-io", - "action.uploading": "Uploading...", - "action.bio.saving": "Saving meow-io...", - "action.account.saving": "Saving cat...", - "action.account.deleting": "Deleting cat...", - "success:profile.updated": "Cat profile updated successfully :3", - "success:avatar.updated": "Cat picture updated successfully :3", - "success:banner.updated": "Cat banner updated successfully :3", - "action.save": "Save :3", - "action.delete.account": "Delete cat", - "account.deleted.scheduled": "Cat scheduled for deletion. It will go to cat heaven after a month of inactivity. Pawing in again will cancel the deletion process :3", - "action.attachment.take.photo": "Meow photo", - "action.attachment.media": "Meowdia", - "action.attachment.file": "Meowle", - "action.attachment.poll": "Meowll", - "action.attachment.uploading": "Uploading meowttachments...", - "attachment.legacy": "Legacy meowttachment", - "warning.unencrypted": "This meowsage is not encrypted", - "action.change.avatar": "Change Cat Picture", - "action.change.banner": "Change Cat Banner", - "placeholder.type.text.emoji": "Type purr / meow...", - "action.cancel": "Cancel", - "title.crop.image": "Crop Image", - "title.zoom": "Zoom", - "title.pin.message": "Pin", - "title.unpin.message": "Unpin", - "message.pinned.by": "{0} pinned a {1}.", - "message.unpinned.by": "{0} unpinned a {1}.", - "title.pinned.messages": "Pinned", - "error.pinned.messages.empty": "No pinned meows", - "title.message.lowercase": "meowsage" -} \ No newline at end of file + "user.not.found": "meowser not found :c", + "dm.already.exists": "direct meowchat already exists :3", + "cant.invite.urself": "you can't invite yourself, silly :3", + "user.already.invited": "you have already invited this user meow", + "user.invited": "user invited successfully :3", + "cant.create.dm.without.invitation": "you can't create a dm without invitation ><", + "dm.begin.notice": "at the beginning of a meowchat, you should always verify that the person you're talking to is the intended recipient", + "failed.accept.dm": "failed to accept direct meowchat", + "dm.accepted": "direct meowchat accepted", + "no.invite.found": "you can't create a dm without invitation *hiss*", + "unknown.error": "unknown error :o", + "missing.keys": "cannot rotate nametags: some kitties don't have public nametags available yet. they must set up their identity.", + "key.rotated": "meowchat keys have been successfully rotated! meow!", + "forbidden": "you do not have pawmission to perform this action *hiss*", + "dm.key.too.large": "meowchat key too large :o", + "dm.key.not.found": "meowchat key not found :c", + "dm.key.updated": "meowchat key updated successfully :3", + "body.too.large": "Meow is too big! Max meow is {0} MB.", + "message.too.long": "Meowssage exceeds maximum allowed size ({0}).", + "reaction.too.long": "Re-meow-ction exceeds maximum allowed size ({0}).", + "invite.revoked": "invite revoked successfully", + "invite.declined": "invite declined successfully", + "account.creation.request.expired": "cat creation request expired. try again", + "invalid.username": "invalid cat name: {0}", + "invalid.password": "invalid meow word: {0}", + "incorrect.captcha": "incorrect captcha. try meowgain", + "username.taken": "this cat name is already taken", + "accounts.slots.full": "you can't create a new cat because all collars are used. try again later", + "registration.disabled": "registration disabled", + "account.created": "cat created successfully", + "password.changed": "meow word changed successfully", + "keys.updated": "nametag updated successfully", + "unknown.request": "unknown request: {0}", + "login.successful": "pawgin successful :3", + "username.length": "cat name must be {0} characters long", + "username.conditions.allowed": "cat name can only include {all}", + "password.not.hashed.properly": "meow word is not hashed properly", + "invalid.username.or.password": "invalid cat name or meow word", + "error.storage.limit.exceeded": "Storage Limit Exceeded, purr", + "error.network.error": "Network error, check connection or file size, meow.", + "account.not.exist": "cat with this name doesn't exist", + "invalid.nonce": "invalid nonce. try again", + "username.changed": "cat name changed successfully", + "auth.failed.redirect.to.login": "failed to find owner. redirecting to pawgin...", + "dm.refresh.failed": "failed to refresh direct meowchats", + "chat.add.failed": "failed to add meowchat", + "something.wrong.mayb.pass": "something went wrong... :c (probably wrong meow word)", + "something.wrong": "something went wrong... :c", + "passwords.not.match": "meow word do not match", + "password.cant.empty": "meow word cannot be empty", + "username.cant.empty": "cat name cannot be empty", + "bad.request": "bad request!!!", + "letters": "letters", + "numbers": "numbers", + "underscores": "underscores", + "loading.connecting": "connecting...", + "loading.loading": "loading...", + "loading.done": "ready! :3", + "title.home": "bed", + "title.dms": "direct meowchats", + "title.groups": "clowder", + "title.welcome.splash": "welpaw to Meowven", + "desc.welcome.splash": "first Larpix client", + "title.splash": "splash", + "title.create.dm": "invite to direct meowchat", + "title.create.space": "create house", + "title.create.group": "create clowder", + "title.add.chat": "add meowchat", + "desc.add.chat": "add a private, encrypted meowchat by entering a cat name", + "desc.create.group": "create a private, encrypted clowder", + "placeholder.create.group.input": "my clowder", + "placeholder.create.space.input": "my house", + "title.name": "name", + "desc.create.space": "create a house for your community", + "desc.join.space": "join a community that fits you", + "title.join.space": "join house", + "title.space.id": "house id", + "title.inbox": "inbox", + "title.invites": "invites", + "title.notifications": "notifications", + "title.received": "received", + "title.sent": "sent", + "title.all": "all", + "title.unread": "unread", + "desc.no.invites": "no invites found :c", + "desc.no.notifications": "no notifications found :c", + "desc.fetching.invites": "fetching invites...", + "desc.fetching.notifications": "fetching notifications...", + "desc.invite.dm.received": "{0} ({1}) invited you to meowchat", + "desc.invite.dm.sent": "you invited {0} ({1}) to meowchat", + "desc.invite.group.received": "{0} ({1}) invited you to a clowder", + "desc.invite.group.sent": "you invited {0} ({1}) to a clowder", + "action.invite.sending": "Sending mewvite...", + "action.processing": "Purrcessing...", + "action.fetching.invites.sent": "Fetching sent mewvites...", + "action.fetching.invites.recv": "fetching received invites...", + "action.dm.fetch": "fetching direct meowchats...", + "action.auth": "authenticating...", + "action.dm.adding": "adding...", + "action.invite.revoking": "revoking invite...", + "action.invite.accepting": "accepting invite...", + "action.invite.declining": "declining invite...", + "title.sign.up": "sign up", + "title.sign.in": "sign in", + "title.sign.in.to": "sign in to your larpix instance", + "title.server.host": "server host", + "title.username": "meow name", + "title.password": "meow word", + "title.new.here.?": "new here?", + "title.create.an.account": "create a cat", + "join.larpix": "join Larpix", + "title.create.new.account": "create new larpix cat account", + "notice.use.strong.pass": "remember to use a strong meow word! tt will be used to lock your collar", + "title.confirm.password": "confirm meow word", + "title.already.registered.?": "already a kitty?", + "title.back.to.login": "back to pawgin", + "title.verify": "verify", + "title.captcha.desc": "prove you are a kitty", + "title.captcha.code": "captcha code", + "title.invitation.code": "invitation code", + "title.back.to.register": "back to registration", + "title.details": "details", + "title.members": "cats", + "placeholder.username": "cat name", + "placeholder.captcha.code": "captcha code", + "desc.messages.loading": "loading meows...", + "desc.no.dms": "no direct meowchats :c", + "action.dm.opening": "opening meowchat...", + "action.group.opening": "opening clowder...", + "dm.open.failed": "failed to open meowchat :c", + "keys.local.server.mismatch": "this device has different nametags than server :c use the device u first logged in on", + "keys.server.decrypt.failed": "wrong password for ur secret nametag bundle :c", + "dm.messages.fetch.failed": "failed to fetch meows :c", + "messages.decrypt.failed": "failed to decrypt meow :c", + "title.reply.message": "meow back", + "title.react.message": "purr", + "title.text.react": "text purr", + "title.delete.message": "hiss away", + "title.replied.to": "meowed back to", + "title.replying.to": "meowing back to", + "action.message.deleting": "hissing message away...", + "action.message.sending": "sending meow...", + "action.message.reacting": "adding purr...", + "error:message.delete.failed": "failed to hiss away meow", + "error:message.send.failed": "failed to send meow", + "error:message.react.failed": "failed to add purr", + "info.sending.message": "sending...", + "placeholder.message.input": "meow...", + "larp.redacted": "Meow meow was meowed.", + "message.already.deleted": "This meow has already been meowed.", + "rate.limit": "You are meowing too quickly. Please meow again later.", + "placeholder.invitation.code": "meow meow meow", + "info.messages.loading.older": "Loading older meowsages...", + "error:message.not.found": "Meowsage not found", + "title.profile": "cat profile", + "title.settings": "cat settings", + "title.logout": "log out", + "title.loading": "sniffing...", + "action.edit.profile": "edit cat profile", + "action.open.dm": "open direct meowchat", + "action.invite.accept": "accept meow invite", + "action.invite.cancel": "hiss away invite", + "action.invite.send": "invite to meowchat", + "action.profile.loading": "sniffing profile...", + "error.profile.load.failed": "failed to sniff profile.", + "unknown.author": "somecat", + "title.account": "Cat", + "title.delete.account": "Delete cat", + "desc.delete.account": "Once you delete your cat, it will go to cat heaven. Please be certain :c", + "title.avatar": "Cat picture", + "title.banner": "Cat banner", + "title.bio": "Cat bio", + "title.appearance": "Appearance", + "title.language": "Meownguage", + "action.save.profile": "Save cat profile", + "action.save.bio": "Save meow-io", + "action.uploading": "Uploading...", + "action.bio.saving": "Saving meow-io...", + "action.account.saving": "Saving cat...", + "action.account.deleting": "Deleting cat...", + "success:profile.updated": "Cat profile updated successfully :3", + "success:avatar.updated": "Cat picture updated successfully :3", + "success:banner.updated": "Cat banner updated successfully :3", + "action.save": "Save :3", + "action.delete.account": "Delete cat", + "account.deleted.scheduled": "Cat scheduled for deletion. It will go to cat heaven after a month of inactivity. Pawing in again will cancel the deletion process :3", + "action.attachment.take.photo": "Meow photo", + "action.attachment.media": "Meowdia", + "action.attachment.file": "Meowle", + "action.attachment.poll": "Meowll", + "action.attachment.uploading": "Uploading meowttachments...", + "attachment.legacy": "Legacy meowttachment", + "warning.unencrypted": "This meowsage is not encrypted", + "action.change.avatar": "Change Cat Picture", + "action.change.banner": "Change Cat Banner", + "placeholder.type.text.emoji": "Type purr / meow...", + "action.cancel": "Cancel", + "title.crop.image": "Crop Image", + "title.zoom": "Zoom", + "title.pin.message": "Pin", + "title.unpin.message": "Unpin", + "message.pinned.by": "{0} pinned a {1}.", + "message.unpinned.by": "{0} unpinned a {1}.", + "title.pinned.messages": "Pinned", + "error.pinned.messages.empty": "No pinned meows", + "title.message.lowercase": "meowsage", + "action.rotate.keys": "Meowtate Keys", + "already.in.group": "Meowready in group", + "desc.key.rotation.info": "Meow need to purrotate meowcription keys to purrtinue sending meowsages in this group. Other meowmbers will autopurrtically receive the new key.", + "desc.no.groups": "Meow are not in any groups yet.", + "dm.read": "Purread", + "empty.body": "Empty meowdy", + "failed.create.group": "Failed to purreate group", + "federation.not.implemented": "Meowderation not implemeowted", + "group.not.found": "Group not meownd", + "group.read": "Purread", + "invalid.entry.name": "Invalid purrtry name", + "invalid.key.index": "Invalid key index (purrlease purrefresh the page)", + "invalid.msg.id": "Invalid meowsage ID", + "invalid.payload": "Invalid purrload", + "invalid.reaction.id": "Invalid purreaction ID", + "invalid.user": "Invalid meowser", + "joined": "Purroined", + "key.rotation.required": "Key purrotation meowquired", + "key.too.large": "Key too meowrge", + "keys.public.mismatch": "Public keys mismemeow", + "label.members": "MEOWMBERS", + "message.deleted": "Meowsage purrleted", + "message.not.found": "Meowsage not meownd", + "message.pin.toggled": "Meowsage purrin purrggled", + "message.reacted": "Meowsage purreacted", + "message.reaction.removed": "Purreaction remeowed", + "no.invite": "No meowvite", + "not.a.member": "Not a meowmber", + "not.in.group": "Not in purroup", + "reaction.not.found": "Purreaction not meownd", + "rotation.not.required": "Purrotation not meowquired", + "storage.entry.updated": "Storage purrtry updated", + "storage.limit.exceeded": "Storage limit exmeowded", + "title.key.rotation": "Key Purrotation", + "error:messages.decrypt.failed": "Depurryption meowled", + "error.message.already.deleted": "Meowsage alpurrdy meowleted", + "group.user.joined.by": "{0} purroined the group", + "group.created.by": "Group purreated by {0}", + "group.user.left.by": "{0} left the purroup", + "action.group.fetch": "Meowtching groups", + "group.created": "Group purreated", + "group.user.joined": "Purroined group", + "prompt.invite.username": "Enter username to invite meow:", + "desc.failed.load.members": "Failed to load members meow", + "prompt.are.you.sure": "Are you sure meow?", + "title.unknown.group": "Unknown Group meow", + "title.unknown": "Unknown meow", + "en-us": "English meow", + "pl-pl": "Polish meow", + "en-cat": "Meowlish", + "avatar.updated": "Avatar updated successfully", + "profile.updated": "Profile updated successfully", + "banner.updated": "Banner updated successfully" +} diff --git a/webroot/blah/en-us.json b/webroot/blah/en-us.json index 21b5fcf2..19aa86ea 100644 --- a/webroot/blah/en-us.json +++ b/webroot/blah/en-us.json @@ -1,205 +1,261 @@ { - "user.not.found": "User not found", - "dm.already.exists": "Direct message already exists", - "cant.invite.urself": "You can't invite yourself", - "user.already.invited": "You have already invited this user", - "user.invited": "User invited successfully", - "cant.create.dm.without.invitation": "You can't create a dm without invitation", - "dm.begin.notice": "At the beginning of a DM, you should always verify that the person you're talking to is the intended recipient", - "failed.accept.dm": "Failed to accept DM", - "dm.accepted": "DM accepted", - "no.invite.found": "You can't create a dm without invitation", - "unknown.error": "Unknown error", - "forbidden": "You do not have permission to perform this action", - "dm.key.too.large": "DM key too large", - "dm.key.not.found": "DM key not found", - "dm.key.updated": "DM key updated successfully", - "body.too.large": "Request too large. Max limit is {0} MB", - "message.too.long": "Message exceeds maximum allowed size ({0}).", - "reaction.too.long": "Reaction exceeds maximum allowed size ({0}).", - "invite.revoked": "Invite revoked successfully", - "invite.declined": "Invite declined successfully", - "account.creation.request.expired": "Account creation request expired. Try again", - "invalid.username": "Invalid username: {0}", - "invalid.password": "Invalid password: {0}", - "incorrect.captcha": "Incorrect captcha. Try again", - "username.taken": "This username is already taken", - "accounts.slots.full": "You can't create a new account because all ids are used. Try again later", - "registration.disabled": "Registration disabled", - "account.created": "Account created successfully", - "password.changed": "Password changed successfully", - "keys.updated": "Keys updated successfully", - "unknown.request": "Unknown request: {0}", - "login.successful": "Login successful", - "username.length": "Username must be {0} characters long", - "username.conditions.allowed": "Username can only include {all}", - "password.not.hashed.properly": "Password is not hashed properly", - "invalid.username.or.password": "Invalid username or password", - "error.storage.limit.exceeded": "Storage Limit Exceeded", - "error.network.error": "Network error, check connection or file size.", - "account.not.exist": "Account with this name doesn't exist", - "invalid.nonce": "Invalid nonce. Try again", - "username.changed": "Username changed successfully", - "auth.failed.redirect.to.login": "Failed to auth. Redirecting to login...", - "dm.refresh.failed": "Failed to refresh dms", - "chat.add.failed": "Failed to add chat", - "something.wrong.mayb.pass": "Something went wrong... (probably wrong password)", - "something.wrong": "Something went wrong...", - "passwords.not.match": "Passwords do not match", - "password.cant.empty": "Password cannot be empty", - "username.cant.empty": "Username cannot be empty", - "bad.request": "Bad request", - "letters": "letters", - "numbers": "numbers", - "underscores": "underscores", - "loading.connecting": "Connecting...", - "loading.loading": "Loading...", - "loading.done": "Ready!", - "title.home": "Home", - "title.dms": "Direct messages", - "title.groups": "Groups", - "title.welcome.splash": "Welcome to Miarven", - "desc.welcome.splash": "First Larpix client", - "title.splash": "Splash", - "title.create.dm": "Invite to dm", - "title.create.space": "Create space", - "title.create.group": "Create group", - "title.add.chat": "Add chat", - "desc.add.chat": "Add a private, encrypted chat by entering a username", - "desc.create.group": "Create a private, encrypted group", - "placeholder.create.group.input": "My group", - "placeholder.create.space.input": "My space", - "title.name": "Name", - "desc.create.space": "Create a space for your community", - "desc.join.space": "Join a community that fits you", - "title.join.space": "Join space", - "title.space.id": "Space id", - "title.inbox": "Inbox", - "title.invites": "Invites", - "title.notifications": "Notifications", - "title.received": "Received", - "title.sent": "Sent", - "title.all": "All", - "title.unread": "Unread", - "desc.no.invites": "No invites found", - "desc.no.notifications": "No notifications found", - "desc.fetching.invites": "Fetching invites...", - "desc.fetching.notifications": "Fetching notifications...", - "desc.invite.dm.received": "{0} ({1}) invited you to chat", - "desc.invite.dm.sent": "You invited {0} ({1}) to chat", - "desc.invite.group.received": "{0} ({1}) invited you to a group", - "desc.invite.group.sent": "You invited {0} ({1}) to a group", - "action.invite.sending": "Sending invite...", - "action.processing": "Processing...", - "action.fetching.invites.sent": "Fetching sent invites...", - "action.fetching.invites.recv": "Fetching received invites...", - "action.dm.fetch": "Fetching dms...", - "action.auth": "Authenticating...", - "action.dm.adding": "Adding...", - "action.invite.revoking": "Revoking invite...", - "action.invite.accepting": "Accepting invite...", - "action.invite.declining": "Declining invite...", - "title.sign.up": "Sign Up", - "title.sign.in": "Sign In", - "title.sign.in.to": "Sign in to your larpix instance", - "title.server.host": "Server Host", - "title.username": "Username", - "title.password": "Password", - "title.new.here.?": "New here?", - "title.create.an.account": "Create an account", - "join.larpix": "Join Larpix", - "title.create.new.account": "Create new larpix account", - "notice.use.strong.pass": "Remember to use a strong password! It will be used to encrypt your keys", - "title.confirm.password": "Confirm Password", - "title.already.registered.?": "Already a member?", - "title.back.to.login": "Back to Login", - "title.verify": "Verify", - "title.captcha.desc": "Prove you are a human", - "title.captcha.code": "Captcha code", - "title.invitation.code": "Invitation code", - "title.back.to.register": "Back to registration", - "placeholder.username": "username", - "placeholder.captcha.code": "captcha code", - "placeholder.message.input": "Message...", - "desc.messages.loading": "Fetching messages...", - "desc.no.dms": "No direct messages", - "action.dm.opening": "Opening DM...", - "dm.open.failed": "Failed to open DM", - "keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.", - "keys.server.decrypt.failed": "Could not decrypt your account keys with this password.", - "dm.messages.fetch.failed": "Failed to fetch messages", - "messages.decrypt.failed": "Failed to decrypt message", - "title.reply.message": "Reply", - "title.react.message": "React", - "title.text.react": "Text React", - "title.delete.message": "Delete", - "title.replied.to": "Replied to", - "title.replying.to": "Replying to", - "action.message.deleting": "Deleting message...", - "action.message.sending": "Sending message...", - "action.message.reacting": "Adding reaction...", - "error:message.delete.failed": "Failed to delete message", - "error:message.send.failed": "Failed to send message", - "error:message.react.failed": "Failed to add reaction", - "info.sending.message": "Sending...", - "larp.redacted": "This message was deleted.", - "message.already.deleted": "This message has already been deleted.", - "rate.limit": "You are sending requests too quickly. Please try again later.", - "placeholder.invitation.code": "invitation code", - "info.messages.loading.older": "Loading older messages...", - "error:message.not.found": "Message not found", - "title.profile": "Profile", - "title.settings": "Settings", - "title.logout": "Log out", - "title.loading": "Loading...", - "title.details": "Details", - "title.members": "Members", - "action.edit.profile": "Edit profile", - "action.open.dm": "Open DM", - "action.invite.accept": "Accept invite", - "action.invite.cancel": "Cancel invite", - "action.invite.send": "Invite to DM", - "action.profile.loading": "Loading profile...", - "error.profile.load.failed": "Failed to load profile.", - "unknown.author": "Someone", - "title.account": "Account", - "title.delete.account": "Delete account", - "desc.delete.account": "Once you delete your account, there is no going back. Please be certain.", - "title.avatar": "Avatar", - "title.banner": "Banner", - "title.bio": "Bio", - "title.appearance": "Appearance", - "title.language": "Language", - "action.save.profile": "Save profile", - "action.save.bio": "Save bio", - "action.uploading": "Uploading...", - "action.bio.saving": "Saving bio...", - "action.account.saving": "Saving account...", - "action.account.deleting": "Deleting account...", - "profile.updated": "Profile updated successfully", - "avatar.updated": "Avatar updated successfully", - "banner.updated": "Banner updated successfully", - "action.save": "Save", - "action.delete.account": "Delete account", - "account.deleted.scheduled": "Account scheduled for deletion. It will be permanently deleted after a month of inactivity. Logging in during this time will cancel the deletion process.", - "action.attachment.take.photo": "Take photo", - "action.attachment.media": "Media", - "action.attachment.file": "File", - "action.attachment.poll": "Poll", - "action.attachment.uploading": "Uploading attachments...", - "attachment.legacy": "Legacy attachment", - "warning.unencrypted": "This message is not encrypted", - "action.change.avatar": "Change Avatar", - "action.change.banner": "Change Banner", - "placeholder.type.text.emoji": "Type text / emoji...", - "action.cancel": "Cancel", - "title.crop.image": "Crop Image", - "title.zoom": "Zoom", - "title.pin.message": "Pin", - "title.unpin.message": "Unpin", - "message.pinned.by": "{0} pinned a {1}.", - "message.unpinned.by": "{0} unpinned a {1}.", - "title.pinned.messages": "Pinned", - "error.pinned.messages.empty": "No pinned messages", - "title.message.lowercase": "message" -} \ No newline at end of file + "user.not.found": "User not found", + "dm.already.exists": "Direct message already exists", + "cant.invite.urself": "You can't invite yourself", + "user.already.invited": "You have already invited this user", + "user.invited": "User invited successfully", + "cant.create.dm.without.invitation": "You can't create a dm without invitation", + "dm.begin.notice": "At the beginning of a DM, you should always verify that the person you're talking to is the intended recipient", + "failed.accept.dm": "Failed to accept DM", + "dm.accepted": "DM accepted", + "no.invite.found": "You can't create a dm without invitation", + "unknown.error": "Unknown error", + "missing.keys": "Cannot rotate keys: Some members don't have public keys available yet. They must configure their identity.", + "key.rotated": "Group keys have been successfully rotated.", + "forbidden": "You do not have permission to perform this action", + "dm.key.too.large": "DM key too large", + "dm.key.not.found": "DM key not found", + "dm.key.updated": "DM key updated successfully", + "body.too.large": "Request too large. Max limit is {0} MB", + "message.too.long": "Message exceeds maximum allowed size ({0}).", + "reaction.too.long": "Reaction exceeds maximum allowed size ({0}).", + "invite.revoked": "Invite revoked successfully", + "invite.declined": "Invite declined successfully", + "account.creation.request.expired": "Account creation request expired. Try again", + "invalid.username": "Invalid username: {0}", + "invalid.password": "Invalid password: {0}", + "incorrect.captcha": "Incorrect captcha. Try again", + "username.taken": "This username is already taken", + "accounts.slots.full": "You can't create a new account because all ids are used. Try again later", + "registration.disabled": "Registration disabled", + "account.created": "Account created successfully", + "password.changed": "Password changed successfully", + "keys.updated": "Keys updated successfully", + "unknown.request": "Unknown request: {0}", + "login.successful": "Login successful", + "username.length": "Username must be {0} characters long", + "username.conditions.allowed": "Username can only include {all}", + "password.not.hashed.properly": "Password is not hashed properly", + "invalid.username.or.password": "Invalid username or password", + "error.storage.limit.exceeded": "Storage Limit Exceeded", + "error.network.error": "Network error, check connection or file size.", + "account.not.exist": "Account with this name doesn't exist", + "invalid.nonce": "Invalid nonce. Try again", + "username.changed": "Username changed successfully", + "auth.failed.redirect.to.login": "Failed to auth. Redirecting to login...", + "dm.refresh.failed": "Failed to refresh dms", + "chat.add.failed": "Failed to add chat", + "something.wrong.mayb.pass": "Something went wrong... (probably wrong password)", + "something.wrong": "Something went wrong...", + "passwords.not.match": "Passwords do not match", + "password.cant.empty": "Password cannot be empty", + "username.cant.empty": "Username cannot be empty", + "bad.request": "Bad request", + "letters": "letters", + "numbers": "numbers", + "underscores": "underscores", + "loading.connecting": "Connecting...", + "loading.loading": "Loading...", + "loading.done": "Ready!", + "title.home": "Home", + "title.dms": "Direct messages", + "title.groups": "Groups", + "title.welcome.splash": "Welcome to Miarven", + "desc.welcome.splash": "First Larpix client", + "title.splash": "Splash", + "title.create.dm": "Invite to dm", + "title.create.space": "Create space", + "title.create.group": "Create group", + "title.add.chat": "Add chat", + "desc.add.chat": "Add a private, encrypted chat by entering a username", + "desc.create.group": "Create a private, encrypted group", + "placeholder.create.group.input": "My group", + "placeholder.create.space.input": "My space", + "title.name": "Name", + "desc.create.space": "Create a space for your community", + "desc.join.space": "Join a community that fits you", + "title.join.space": "Join space", + "title.space.id": "Space id", + "title.inbox": "Inbox", + "title.invites": "Invites", + "title.notifications": "Notifications", + "title.received": "Received", + "title.sent": "Sent", + "title.all": "All", + "title.unread": "Unread", + "desc.no.invites": "No invites found", + "desc.no.notifications": "No notifications found", + "desc.fetching.invites": "Fetching invites...", + "desc.fetching.notifications": "Fetching notifications...", + "desc.invite.dm.received": "{0} ({1}) invited you to chat", + "desc.invite.dm.sent": "You invited {0} ({1}) to chat", + "desc.invite.group.received": "{0} ({1}) invited you to a group", + "desc.invite.group.sent": "You invited {0} ({1}) to a group", + "action.invite.sending": "Sending invite...", + "action.processing": "Processing...", + "action.fetching.invites.sent": "Fetching sent invites...", + "action.fetching.invites.recv": "Fetching received invites...", + "action.dm.fetch": "Fetching dms...", + "action.auth": "Authenticating...", + "action.dm.adding": "Adding...", + "action.invite.revoking": "Revoking invite...", + "action.invite.accepting": "Accepting invite...", + "action.invite.declining": "Declining invite...", + "title.sign.up": "Sign Up", + "title.sign.in": "Sign In", + "title.sign.in.to": "Sign in to your larpix instance", + "title.server.host": "Server Host", + "title.username": "Username", + "title.password": "Password", + "title.new.here.?": "New here?", + "title.create.an.account": "Create an account", + "join.larpix": "Join Larpix", + "title.create.new.account": "Create new larpix account", + "notice.use.strong.pass": "Remember to use a strong password! It will be used to encrypt your keys", + "title.confirm.password": "Confirm Password", + "title.already.registered.?": "Already a member?", + "title.back.to.login": "Back to Login", + "title.verify": "Verify", + "title.captcha.desc": "Prove you are a human", + "title.captcha.code": "Captcha code", + "title.invitation.code": "Invitation code", + "title.back.to.register": "Back to registration", + "placeholder.username": "username", + "placeholder.captcha.code": "captcha code", + "placeholder.message.input": "Message...", + "desc.messages.loading": "Fetching messages...", + "desc.no.dms": "No direct messages", + "action.dm.opening": "Opening DM...", + "action.group.opening": "Opening group...", + "dm.open.failed": "Failed to open DM", + "keys.local.server.mismatch": "This device has different encryption keys than the server. Use the device where you first logged in, or restore server keys from backup.", + "keys.server.decrypt.failed": "Could not decrypt your account keys with this password.", + "dm.messages.fetch.failed": "Failed to fetch messages", + "messages.decrypt.failed": "Failed to decrypt message", + "title.reply.message": "Reply", + "title.react.message": "React", + "title.text.react": "Text React", + "title.delete.message": "Delete", + "title.replied.to": "Replied to", + "title.replying.to": "Replying to", + "action.message.deleting": "Deleting message...", + "action.message.sending": "Sending message...", + "action.message.reacting": "Adding reaction...", + "error:message.delete.failed": "Failed to delete message", + "error:message.send.failed": "Failed to send message", + "error:message.react.failed": "Failed to add reaction", + "info.sending.message": "Sending...", + "larp.redacted": "This message was deleted.", + "message.already.deleted": "This message has already been deleted.", + "rate.limit": "You are sending requests too quickly. Please try again later.", + "placeholder.invitation.code": "invitation code", + "info.messages.loading.older": "Loading older messages...", + "error:message.not.found": "Message not found", + "title.profile": "Profile", + "title.settings": "Settings", + "title.logout": "Log out", + "title.loading": "Loading...", + "title.details": "Details", + "title.members": "Members", + "action.edit.profile": "Edit profile", + "action.open.dm": "Open DM", + "action.invite.accept": "Accept invite", + "action.invite.cancel": "Cancel invite", + "action.invite.send": "Invite to DM", + "action.profile.loading": "Loading profile...", + "error.profile.load.failed": "Failed to load profile.", + "unknown.author": "Someone", + "title.account": "Account", + "title.delete.account": "Delete account", + "desc.delete.account": "Once you delete your account, there is no going back. Please be certain.", + "title.avatar": "Avatar", + "title.banner": "Banner", + "title.bio": "Bio", + "title.appearance": "Appearance", + "title.language": "Language", + "action.save.profile": "Save profile", + "action.save.bio": "Save bio", + "action.uploading": "Uploading...", + "action.bio.saving": "Saving bio...", + "action.account.saving": "Saving account...", + "action.account.deleting": "Deleting account...", + "profile.updated": "Profile updated successfully", + "avatar.updated": "Avatar updated successfully", + "banner.updated": "Banner updated successfully", + "action.save": "Save", + "action.delete.account": "Delete account", + "account.deleted.scheduled": "Account scheduled for deletion. It will be permanently deleted after a month of inactivity. Logging in during this time will cancel the deletion process.", + "action.attachment.take.photo": "Take photo", + "action.attachment.media": "Media", + "action.attachment.file": "File", + "action.attachment.poll": "Poll", + "action.attachment.uploading": "Uploading attachments...", + "attachment.legacy": "Legacy attachment", + "warning.unencrypted": "This message is not encrypted", + "action.change.avatar": "Change Avatar", + "action.change.banner": "Change Banner", + "placeholder.type.text.emoji": "Type text / emoji...", + "action.cancel": "Cancel", + "title.crop.image": "Crop Image", + "title.zoom": "Zoom", + "title.pin.message": "Pin", + "title.unpin.message": "Unpin", + "message.pinned.by": "{0} pinned a {1}.", + "message.unpinned.by": "{0} unpinned a {1}.", + "title.pinned.messages": "Pinned", + "error.pinned.messages.empty": "No pinned messages", + "title.message.lowercase": "message", + "action.rotate.keys": "Rotate Keys", + "already.in.group": "Already in group", + "desc.key.rotation.info": "You need to rotate encryption keys to continue sending messages in this group. Other members will automatically receive the new key.", + "desc.no.groups": "You are not in any groups yet.", + "dm.read": "Read", + "empty.body": "Empty body", + "failed.create.group": "Failed to create group", + "federation.not.implemented": "Federation not implemented", + "group.not.found": "Group not found", + "group.read": "Read", + "invalid.entry.name": "Invalid entry name", + "invalid.key.index": "Invalid key index (please refresh the page)", + "invalid.msg.id": "Invalid message ID", + "invalid.payload": "Invalid payload", + "invalid.reaction.id": "Invalid reaction ID", + "invalid.user": "Invalid user", + "joined": "Joined", + "key.rotation.required": "Key rotation required", + "key.too.large": "Key too large", + "keys.public.mismatch": "Public keys mismatch", + "label.members": "MEMBERS", + "message.deleted": "Message deleted", + "message.not.found": "Message not found", + "message.pin.toggled": "Message pin toggled", + "message.reacted": "Message reacted", + "message.reaction.removed": "Reaction removed", + "no.invite": "No invite", + "not.a.member": "Not a member", + "not.in.group": "Not in group", + "reaction.not.found": "Reaction not found", + "rotation.not.required": "Rotation not required", + "storage.entry.updated": "Storage entry updated", + "storage.limit.exceeded": "Storage limit exceeded", + "title.key.rotation": "Key Rotation", + "error:messages.decrypt.failed": "Decryption failed", + "error.message.already.deleted": "Message already deleted", + "group.user.joined.by": "{0} joined the group", + "group.created.by": "Group created by {0}", + "group.user.left.by": "{0} left the group", + "action.group.fetch": "Fetching groups", + "group.created": "Group created", + "group.user.joined": "Joined group", + "prompt.invite.username": "Enter username to invite:", + "desc.failed.load.members": "Failed to load members", + "prompt.are.you.sure": "Are you sure?", + "title.unknown.group": "Unknown Group", + "title.unknown": "Unknown", + "success:banner.updated": "Banner updated successfully", + "success:profile.updated": "Profile updated successfully", + "success:avatar.updated": "Avatar updated successfully", + "en-us": "English", + "pl-pl": "Polish", + "en-cat": "Meowlish" +} diff --git a/webroot/blah/pl-pl.json b/webroot/blah/pl-pl.json index 7ab7e4bf..302197a2 100644 --- a/webroot/blah/pl-pl.json +++ b/webroot/blah/pl-pl.json @@ -1,205 +1,261 @@ { - "user.not.found": "Nie znaleziono użytkownika", - "dm.already.exists": "Wiadomość prywatna już istnieje", - "cant.invite.urself": "Nie możesz zaprosić samego siebie", - "user.already.invited": "Ten użytkownik został już przez Ciebie zaproszony", - "user.invited": "Użytkownik został pomyślnie zaproszony", - "cant.create.dm.without.invitation": "Nie możesz utworzyć wiadomości prywatnej bez zaproszenia", - "dm.begin.notice": "Na początku każdej rozmowy prywatnej zawsze upewnij się, że rozmawiasz z właściwą osobą", - "failed.accept.dm": "Nie udało się zaakceptować rozmowy prywatnej", - "dm.accepted": "Rozmowa prywatna została zaakceptowana", - "no.invite.found": "Nie możesz utworzyć wiadomości prywatnej bez zaproszenia", - "unknown.error": "Nieznany błąd", - "forbidden": "Nie masz uprawnień do wykonania tej akcji", - "dm.key.too.large": "Klucz wiadomości prywatnej jest zbyt duży", - "dm.key.not.found": "Nie znaleziono klucza wiadomości prywatnej", - "dm.key.updated": "Klucz wiadomości prywatnej został pomyślnie zaktualizowany", - "body.too.large": "Treść żądania jest zbyt duża. Maksymalny limit to {0} MB", - "message.too.long": "Wiadomość przekracza dozwolony rozmiar ({0}).", - "reaction.too.long": "Reakcja przekracza dozwolony rozmiar ({0}).", - "invite.revoked": "Zaproszenie zostało cofnięte", - "invite.declined": "Zaproszenie zostało odrzucone", - "account.creation.request.expired": "Żądanie utworzenia konta wygasło. Spróbuj ponownie", - "invalid.username": "Nieprawidłowa nazwa użytkownika: {0}", - "invalid.password": "Nieprawidłowe hasło: {0}", - "incorrect.captcha": "Nieprawidłowa captcha. Spróbuj ponownie", - "username.taken": "Ta nazwa użytkownika jest już zajęta", - "accounts.slots.full": "Nie można utworzyć nowego konta, ponieważ wszystkie identyfikatory są zajęte. Spróbuj ponownie później", - "registration.disabled": "Rejestracja jest wyłączona", - "account.created": "Konto zostało pomyślnie utworzone", - "password.changed": "Hasło zostało pomyślnie zmienione", - "keys.updated": "Klucze zostały pomyślnie zaktualizowane", - "unknown.request": "Nieznane żądanie: {0}", - "login.successful": "Logowanie zakończone pomyślnie", - "username.length": "Nazwa użytkownika musi mieć {0} znaków", - "username.conditions.allowed": "Nazwa użytkownika może zawierać tylko: {all}", - "password.not.hashed.properly": "Hasło nie zostało poprawnie zahaszowane", - "invalid.username.or.password": "Nieprawidłowa nazwa użytkownika lub hasło", - "error.storage.limit.exceeded": "Przekroczono limit pamięci", - "error.network.error": "Błąd sieci. Sprawdź połączenie lub rozmiar pliku.", - "account.not.exist": "Konto o tej nazwie nie istnieje", - "invalid.nonce": "Nieprawidłowy nonce. Spróbuj ponownie", - "username.changed": "Nazwa użytkownika została pomyślnie zmieniona", - "auth.failed.redirect.to.login": "Uwierzytelnianie nie powiodło się. Przekierowywanie do logowania...", - "dm.refresh.failed": "Nie udało się odświeżyć rozmów prywatnych", - "chat.add.failed": "Nie udało się dodać czatu", - "something.wrong.mayb.pass": "Coś poszło nie tak... (prawdopodobnie błędne hasło)", - "something.wrong": "Coś poszło nie tak...", - "passwords.not.match": "Hasła nie są zgodne", - "password.cant.empty": "Hasło nie może być puste", - "username.cant.empty": "Nazwa użytkownika nie może być pusta", - "bad.request": "Nieprawidłowe żądanie", - "letters": "litery", - "numbers": "cyfry", - "underscores": "podkreślenia", - "loading.connecting": "Łączenie...", - "loading.loading": "Ładowanie...", - "loading.done": "Gotowe!", - "title.home": "Strona główna", - "title.dms": "Wiadomości prywatne", - "title.groups": "Grupy", - "title.welcome.splash": "Witamy w Miarven", - "desc.welcome.splash": "Pierwszy klient Larpix", - "title.splash": "Ekran powitalny", - "title.create.dm": "Zaproś do rozmowy prywatnej", - "title.create.space": "Utwórz społeczność", - "title.create.group": "Utwórz grupę", - "title.add.chat": "Dodaj czat", - "desc.add.chat": "Dodaj prywatny, szyfrowany czat, wpisując nazwę użytkownika", - "desc.create.group": "Utwórz prywatną, szyfrowaną grupę", - "placeholder.create.group.input": "Moja grupa", - "placeholder.create.space.input": "Moja społeczność", - "title.name": "Nazwa", - "desc.create.space": "Utwórz społeczność dla swojej grupy", - "desc.join.space": "Dołącz do społeczności, która Ci odpowiada", - "title.join.space": "Dołącz do społeczności", - "title.space.id": "Identyfikator społeczności", - "title.inbox": "Skrzynka odbiorcza", - "title.invites": "Zaproszenia", - "title.notifications": "Powiadomienia", - "title.received": "Odebrane", - "title.sent": "Wysłane", - "title.all": "Wszystkie", - "title.unread": "Nieprzeczytane", - "desc.no.invites": "Brak zaproszeń", - "desc.no.notifications": "Brak powiadomień", - "desc.fetching.invites": "Pobieranie zaproszeń...", - "desc.fetching.notifications": "Pobieranie powiadomień...", - "desc.invite.dm.received": "{0} ({1}) zaprosił(a) Cię do rozmowy", - "desc.invite.dm.sent": "Zaprosiłeś(-aś) {0} ({1}) do rozmowy", - "desc.invite.group.received": "{0} ({1}) zaprosił(a) Cię do grupy", - "desc.invite.group.sent": "Zaprosiłeś(-aś) {0} ({1}) do grupy", - "action.invite.sending": "Wysyłanie zaproszenia...", - "action.processing": "Przetwarzanie...", - "action.fetching.invites.sent": "Pobieranie wysłanych zaproszeń...", - "action.fetching.invites.recv": "Pobieranie otrzymanych zaproszeń...", - "action.dm.fetch": "Pobieranie rozmów prywatnych...", - "action.auth": "Uwierzytelnianie...", - "action.dm.adding": "Dodawanie...", - "action.invite.revoking": "Cofanie zaproszenia...", - "action.invite.accepting": "Akceptowanie zaproszenia...", - "action.invite.declining": "Odrzucanie zaproszenia...", - "title.sign.up": "Zarejestruj się", - "title.sign.in": "Zaloguj się", - "title.sign.in.to": "Zaloguj się do swojej instancji Larpix", - "title.server.host": "Adres serwera", - "title.username": "Nazwa użytkownika", - "title.password": "Hasło", - "title.new.here.?": "Nowy tutaj?", - "title.create.an.account": "Utwórz konto", - "join.larpix": "Dołącz do Larpix", - "title.create.new.account": "Utwórz nowe konto Larpix", - "notice.use.strong.pass": "Pamiętaj o użyciu silnego hasła! Posłuży ono do szyfrowania Twoich kluczy.", - "title.confirm.password": "Potwierdź hasło", - "title.already.registered.?": "Masz już konto?", - "title.back.to.login": "Powrót do logowania", - "title.verify": "Weryfikacja", - "title.captcha.desc": "Udowodnij, że jesteś człowiekiem", - "title.captcha.code": "Kod CAPTCHA", - "title.invitation.code": "Kod zaproszenia", - "title.back.to.register": "Powrót do rejestracji", - "placeholder.username": "nazwa użytkownika", - "placeholder.captcha.code": "kod captcha", - "placeholder.message.input": "Wiadomość...", - "desc.messages.loading": "Pobieranie wiadomości...", - "desc.no.dms": "Brak wiadomości prywatnych", - "action.dm.opening": "Otwieranie rozmowy...", - "dm.open.failed": "Nie udało się otworzyć rozmowy", - "keys.local.server.mismatch": "To urządzenie ma inne klucze szyfrowania niż serwer. Użyj urządzenia, na którym zalogowano się po raz pierwszy, lub przywróć klucze z kopii zapasowej.", - "keys.server.decrypt.failed": "Nie udało się odszyfrować kluczy konta przy użyciu tego hasła.", - "dm.messages.fetch.failed": "Nie udało się pobrać wiadomości", - "messages.decrypt.failed": "Nie udało się odszyfrować wiadomości", - "title.reply.message": "Odpowiedz", - "title.react.message": "Zareaguj", - "title.text.react": "Reakcja tekstowa", - "title.delete.message": "Usuń", - "title.replied.to": "W odpowiedzi do", - "title.replying.to": "Odpowiadasz na", - "action.message.deleting": "Usuwanie wiadomości...", - "action.message.sending": "Wysyłanie wiadomości...", - "action.message.reacting": "Dodawanie reakcji...", - "error:message.delete.failed": "Nie udało się usunąć wiadomości", - "error:message.send.failed": "Nie udało się wysłać wiadomości", - "error:message.react.failed": "Nie udało się dodać reakcji", - "info.sending.message": "Wysyłanie...", - "larp.redacted": "Ta wiadomość została usunięta.", - "message.already.deleted": "Ta wiadomość została już usunięta.", - "rate.limit": "Wysyłasz żądania zbyt szybko. Spróbuj ponownie później.", - "placeholder.invitation.code": "kod zaproszenia", - "info.messages.loading.older": "Ładowanie starszych wiadomości...", - "error:message.not.found": "Nie znaleziono wiadomości", - "title.profile": "Profil", - "title.settings": "Ustawienia", - "title.logout": "Wyloguj", - "title.loading": "Ładowanie...", - "title.details": "Szczegóły", - "title.members": "Członkowie", - "action.edit.profile": "Edytuj profil", - "action.open.dm": "Otwórz rozmowę", - "action.invite.accept": "Akceptuj zaproszenie", - "action.invite.cancel": "Anuluj zaproszenie", - "action.invite.send": "Zaproś do rozmowy", - "action.profile.loading": "Ładowanie profilu...", - "error.profile.load.failed": "Nie udało się załadować profilu.", - "unknown.author": "Ktoś", - "title.account": "Konto", - "title.delete.account": "Usuń konto", - "desc.delete.account": "Po usunięciu konta nie będzie możliwości jego odzyskania. Upewnij się, że na pewno chcesz to zrobić.", - "title.avatar": "Awatar", - "title.banner": "Baner", - "title.bio": "Opis", - "title.appearance": "Wygląd", - "title.language": "Język", - "action.save.profile": "Zapisz profil", - "action.save.bio": "Zapisz opis", - "action.uploading": "Przesyłanie...", - "action.bio.saving": "Zapisywanie opisu...", - "action.account.saving": "Zapisywanie konta...", - "action.account.deleting": "Usuwanie konta...", - "profile.updated": "Profil został pomyślnie zaktualizowany", - "avatar.updated": "Awatar został pomyślnie zaktualizowany", - "banner.updated": "Baner został pomyślnie zaktualizowany", - "action.save": "Zapisz", - "action.delete.account": "Usuń konto", - "account.deleted.scheduled": "Konto zostało oznaczone do usunięcia. Zostanie trwale usunięte po miesiącu braku aktywności. Zalogowanie się w tym czasie anuluje proces usuwania.", - "action.attachment.take.photo": "Zrób zdjęcie", - "action.attachment.media": "Multimedia", - "action.attachment.file": "Plik", - "action.attachment.poll": "Ankieta", - "action.attachment.uploading": "Przesyłanie załączników...", - "attachment.legacy": "Starszy załącznik", - "warning.unencrypted": "Ta wiadomość nie jest szyfrowana", - "action.change.avatar": "Zmień awatar", - "action.change.banner": "Zmień baner", - "placeholder.type.text.emoji": "Wpisz tekst / emoji...", - "action.cancel": "Anuluj", - "title.crop.image": "Przytnij obraz", - "title.zoom": "Przybliżenie", - "title.pin.message": "Przypnij", - "title.unpin.message": "Odepnij", - "message.pinned.by": "{0} przypiął {1}.", - "message.unpinned.by": "{0} odpiął {1}.", - "title.pinned.messages": "Przypięte", - "error.pinned.messages.empty": "Brak przypiętych wiadomości", - "title.message.lowercase": "wiadomość" -} \ No newline at end of file + "user.not.found": "Nie znaleziono użytkownika", + "dm.already.exists": "Wiadomość prywatna już istnieje", + "cant.invite.urself": "Nie możesz zaprosić samego siebie", + "user.already.invited": "Ten użytkownik został już przez Ciebie zaproszony", + "user.invited": "Użytkownik został pomyślnie zaproszony", + "cant.create.dm.without.invitation": "Nie możesz utworzyć wiadomości prywatnej bez zaproszenia", + "dm.begin.notice": "Na początku każdej rozmowy prywatnej zawsze upewnij się, że rozmawiasz z właściwą osobą", + "failed.accept.dm": "Nie udało się zaakceptować rozmowy prywatnej", + "dm.accepted": "Rozmowa prywatna została zaakceptowana", + "no.invite.found": "Nie możesz utworzyć wiadomości prywatnej bez zaproszenia", + "unknown.error": "Nieznany błąd", + "forbidden": "Nie masz uprawnień do wykonania tej akcji", + "dm.key.too.large": "Klucz wiadomości prywatnej jest zbyt duży", + "dm.key.not.found": "Nie znaleziono klucza wiadomości prywatnej", + "dm.key.updated": "Klucz wiadomości prywatnej został pomyślnie zaktualizowany", + "body.too.large": "Treść żądania jest zbyt duża. Maksymalny limit to {0} MB", + "message.too.long": "Wiadomość przekracza dozwolony rozmiar ({0}).", + "reaction.too.long": "Reakcja przekracza dozwolony rozmiar ({0}).", + "invite.revoked": "Zaproszenie zostało cofnięte", + "invite.declined": "Zaproszenie zostało odrzucone", + "account.creation.request.expired": "Żądanie utworzenia konta wygasło. Spróbuj ponownie", + "invalid.username": "Nieprawidłowa nazwa użytkownika: {0}", + "invalid.password": "Nieprawidłowe hasło: {0}", + "incorrect.captcha": "Nieprawidłowa captcha. Spróbuj ponownie", + "username.taken": "Ta nazwa użytkownika jest już zajęta", + "accounts.slots.full": "Nie można utworzyć nowego konta, ponieważ wszystkie identyfikatory są zajęte. Spróbuj ponownie później", + "registration.disabled": "Rejestracja jest wyłączona", + "account.created": "Konto zostało pomyślnie utworzone", + "password.changed": "Hasło zostało pomyślnie zmienione", + "keys.updated": "Klucze zostały pomyślnie zaktualizowane", + "unknown.request": "Nieznane żądanie: {0}", + "login.successful": "Logowanie zakończone pomyślnie", + "username.length": "Nazwa użytkownika musi mieć {0} znaków", + "username.conditions.allowed": "Nazwa użytkownika może zawierać tylko: {all}", + "password.not.hashed.properly": "Hasło nie zostało poprawnie zahaszowane", + "invalid.username.or.password": "Nieprawidłowa nazwa użytkownika lub hasło", + "error.storage.limit.exceeded": "Przekroczono limit pamięci", + "error.network.error": "Błąd sieci. Sprawdź połączenie lub rozmiar pliku.", + "account.not.exist": "Konto o tej nazwie nie istnieje", + "invalid.nonce": "Nieprawidłowy nonce. Spróbuj ponownie", + "username.changed": "Nazwa użytkownika została pomyślnie zmieniona", + "auth.failed.redirect.to.login": "Uwierzytelnianie nie powiodło się. Przekierowywanie do logowania...", + "dm.refresh.failed": "Nie udało się odświeżyć rozmów prywatnych", + "chat.add.failed": "Nie udało się dodać czatu", + "something.wrong.mayb.pass": "Coś poszło nie tak... (prawdopodobnie błędne hasło)", + "something.wrong": "Coś poszło nie tak...", + "passwords.not.match": "Hasła nie są zgodne", + "password.cant.empty": "Hasło nie może być puste", + "username.cant.empty": "Nazwa użytkownika nie może być pusta", + "bad.request": "Nieprawidłowe żądanie", + "letters": "litery", + "numbers": "cyfry", + "underscores": "podkreślenia", + "loading.connecting": "Łączenie...", + "loading.loading": "Ładowanie...", + "loading.done": "Gotowe!", + "title.home": "Strona główna", + "title.dms": "Wiadomości prywatne", + "title.groups": "Grupy", + "title.welcome.splash": "Witamy w Miarven", + "desc.welcome.splash": "Pierwszy klient Larpix", + "title.splash": "Ekran powitalny", + "title.create.dm": "Zaproś do rozmowy prywatnej", + "title.create.space": "Utwórz społeczność", + "title.create.group": "Utwórz grupę", + "title.add.chat": "Dodaj czat", + "desc.add.chat": "Dodaj prywatny, szyfrowany czat, wpisując nazwę użytkownika", + "desc.create.group": "Utwórz prywatną, szyfrowaną grupę", + "placeholder.create.group.input": "Moja grupa", + "placeholder.create.space.input": "Moja społeczność", + "title.name": "Nazwa", + "desc.create.space": "Utwórz społeczność dla swojej grupy", + "desc.join.space": "Dołącz do społeczności, która Ci odpowiada", + "title.join.space": "Dołącz do społeczności", + "title.space.id": "Identyfikator społeczności", + "title.inbox": "Skrzynka odbiorcza", + "title.invites": "Zaproszenia", + "title.notifications": "Powiadomienia", + "title.received": "Odebrane", + "title.sent": "Wysłane", + "title.all": "Wszystkie", + "title.unread": "Nieprzeczytane", + "desc.no.invites": "Brak zaproszeń", + "desc.no.notifications": "Brak powiadomień", + "desc.fetching.invites": "Pobieranie zaproszeń...", + "desc.fetching.notifications": "Pobieranie powiadomień...", + "desc.invite.dm.received": "{0} ({1}) zaprosił(a) Cię do rozmowy", + "desc.invite.dm.sent": "Zaprosiłeś(-aś) {0} ({1}) do rozmowy", + "desc.invite.group.received": "{0} ({1}) zaprosił(a) Cię do grupy", + "desc.invite.group.sent": "Zaprosiłeś(-aś) {0} ({1}) do grupy", + "action.invite.sending": "Wysyłanie zaproszenia...", + "action.processing": "Przetwarzanie...", + "action.fetching.invites.sent": "Pobieranie wysłanych zaproszeń...", + "action.fetching.invites.recv": "Pobieranie otrzymanych zaproszeń...", + "action.dm.fetch": "Pobieranie rozmów prywatnych...", + "action.auth": "Uwierzytelnianie...", + "action.dm.adding": "Dodawanie...", + "action.invite.revoking": "Cofanie zaproszenia...", + "action.invite.accepting": "Akceptowanie zaproszenia...", + "action.invite.declining": "Odrzucanie zaproszenia...", + "title.sign.up": "Zarejestruj się", + "title.sign.in": "Zaloguj się", + "title.sign.in.to": "Zaloguj się do swojej instancji Larpix", + "title.server.host": "Adres serwera", + "title.username": "Nazwa użytkownika", + "title.password": "Hasło", + "title.new.here.?": "Nowy tutaj?", + "title.create.an.account": "Utwórz konto", + "join.larpix": "Dołącz do Larpix", + "title.create.new.account": "Utwórz nowe konto Larpix", + "notice.use.strong.pass": "Pamiętaj o użyciu silnego hasła! Posłuży ono do szyfrowania Twoich kluczy.", + "title.confirm.password": "Potwierdź hasło", + "title.already.registered.?": "Masz już konto?", + "title.back.to.login": "Powrót do logowania", + "title.verify": "Weryfikacja", + "title.captcha.desc": "Udowodnij, że jesteś człowiekiem", + "title.captcha.code": "Kod CAPTCHA", + "title.invitation.code": "Kod zaproszenia", + "title.back.to.register": "Powrót do rejestracji", + "placeholder.username": "nazwa użytkownika", + "placeholder.captcha.code": "kod captcha", + "placeholder.message.input": "Wiadomość...", + "desc.messages.loading": "Pobieranie wiadomości...", + "desc.no.dms": "Brak wiadomości prywatnych", + "action.dm.opening": "Otwieranie rozmowy...", + "action.group.opening": "Otwieranie grupy...", + "dm.open.failed": "Nie udało się otworzyć rozmowy", + "keys.local.server.mismatch": "To urządzenie ma inne klucze szyfrowania niż serwer. Użyj urządzenia, na którym zalogowano się po raz pierwszy, lub przywróć klucze z kopii zapasowej.", + "keys.server.decrypt.failed": "Nie udało się odszyfrować kluczy konta przy użyciu tego hasła.", + "dm.messages.fetch.failed": "Nie udało się pobrać wiadomości", + "messages.decrypt.failed": "Nie udało się odszyfrować wiadomości", + "title.reply.message": "Odpowiedz", + "title.react.message": "Zareaguj", + "title.text.react": "Reakcja tekstowa", + "title.delete.message": "Usuń", + "title.replied.to": "W odpowiedzi do", + "title.replying.to": "Odpowiadasz na", + "action.message.deleting": "Usuwanie wiadomości...", + "action.message.sending": "Wysyłanie wiadomości...", + "action.message.reacting": "Dodawanie reakcji...", + "error:message.delete.failed": "Nie udało się usunąć wiadomości", + "error:message.send.failed": "Nie udało się wysłać wiadomości", + "error:message.react.failed": "Nie udało się dodać reakcji", + "info.sending.message": "Wysyłanie...", + "larp.redacted": "Ta wiadomość została usunięta.", + "message.already.deleted": "Ta wiadomość została już usunięta.", + "rate.limit": "Wysyłasz żądania zbyt szybko. Spróbuj ponownie później.", + "placeholder.invitation.code": "kod zaproszenia", + "info.messages.loading.older": "Ładowanie starszych wiadomości...", + "error:message.not.found": "Nie znaleziono wiadomości", + "title.profile": "Profil", + "title.settings": "Ustawienia", + "title.logout": "Wyloguj", + "title.loading": "Ładowanie...", + "title.details": "Szczegóły", + "title.members": "Członkowie", + "action.edit.profile": "Edytuj profil", + "action.open.dm": "Otwórz rozmowę", + "action.invite.accept": "Akceptuj zaproszenie", + "action.invite.cancel": "Anuluj zaproszenie", + "action.invite.send": "Zaproś do rozmowy", + "action.profile.loading": "Ładowanie profilu...", + "error.profile.load.failed": "Nie udało się załadować profilu.", + "unknown.author": "Ktoś", + "title.account": "Konto", + "title.delete.account": "Usuń konto", + "desc.delete.account": "Po usunięciu konta nie będzie możliwości jego odzyskania. Upewnij się, że na pewno chcesz to zrobić.", + "title.avatar": "Awatar", + "title.banner": "Baner", + "title.bio": "Opis", + "title.appearance": "Wygląd", + "title.language": "Język", + "action.save.profile": "Zapisz profil", + "action.save.bio": "Zapisz opis", + "action.uploading": "Przesyłanie...", + "action.bio.saving": "Zapisywanie opisu...", + "action.account.saving": "Zapisywanie konta...", + "action.account.deleting": "Usuwanie konta...", + "profile.updated": "Profil został pomyślnie zaktualizowany", + "avatar.updated": "Awatar został pomyślnie zaktualizowany", + "banner.updated": "Baner został pomyślnie zaktualizowany", + "action.save": "Zapisz", + "action.delete.account": "Usuń konto", + "account.deleted.scheduled": "Konto zostało oznaczone do usunięcia. Zostanie trwale usunięte po miesiącu braku aktywności. Zalogowanie się w tym czasie anuluje proces usuwania.", + "action.attachment.take.photo": "Zrób zdjęcie", + "action.attachment.media": "Multimedia", + "action.attachment.file": "Plik", + "action.attachment.poll": "Ankieta", + "action.attachment.uploading": "Przesyłanie załączników...", + "attachment.legacy": "Starszy załącznik", + "warning.unencrypted": "Ta wiadomość nie jest szyfrowana", + "action.change.avatar": "Zmień awatar", + "action.change.banner": "Zmień baner", + "placeholder.type.text.emoji": "Wpisz tekst / emoji...", + "action.cancel": "Anuluj", + "title.crop.image": "Przytnij obraz", + "title.zoom": "Przybliżenie", + "title.pin.message": "Przypnij", + "title.unpin.message": "Odepnij", + "message.pinned.by": "{0} przypiął {1}.", + "message.unpinned.by": "{0} odpiął {1}.", + "title.pinned.messages": "Przypięte", + "error.pinned.messages.empty": "Brak przypiętych wiadomości", + "title.message.lowercase": "wiadomość", + "action.rotate.keys": "Rotuj Klucze", + "already.in.group": "Już w grupie", + "desc.key.rotation.info": "Musisz zrotować klucze szyfrujące, aby kontynuować wysyłanie wiadomości w tej grupie. Inni członkowie automatycznie otrzymają nowy klucz.", + "desc.no.groups": "Nie jesteś jeszcze w żadnej grupie.", + "dm.read": "Przeczytano", + "empty.body": "Pusta zawartość", + "failed.create.group": "Nie udało się utworzyć grupy", + "federation.not.implemented": "Federacja niezaimplementowana", + "group.not.found": "Nie znaleziono grupy", + "group.read": "Przeczytano", + "invalid.entry.name": "Nieprawidłowa nazwa wpisu", + "invalid.key.index": "Nieprawidłowy indeks klucza (odśwież stronę)", + "invalid.msg.id": "Nieprawidłowe ID wiadomości", + "invalid.payload": "Nieprawidłowy ładunek", + "invalid.reaction.id": "Nieprawidłowe ID reakcji", + "invalid.user": "Nieprawidłowy użytkownik", + "joined": "Dołączono", + "key.rotation.required": "Wymagana rotacja kluczy", + "key.too.large": "Klucz zbyt duży", + "keys.public.mismatch": "Niezgodność kluczy publicznych", + "label.members": "CZŁONKOWIE", + "message.deleted": "Wiadomość usunięta", + "message.not.found": "Nie znaleziono wiadomości", + "message.pin.toggled": "Zmieniono przypięcie wiadomości", + "message.reacted": "Dodano reakcję", + "message.reaction.removed": "Reakcja usunięta", + "no.invite": "Brak zaproszenia", + "not.a.member": "Nie jesteś członkiem", + "not.in.group": "Nie jesteś w grupie", + "reaction.not.found": "Nie znaleziono reakcji", + "rotation.not.required": "Rotacja niewymagana", + "storage.entry.updated": "Zaktualizowano wpis pamięci", + "storage.limit.exceeded": "Przekroczono limit pamięci", + "title.key.rotation": "Rotacja kluczy", + "missing.keys": "Brakujące klucze", + "error:messages.decrypt.failed": "Deszyfrowanie nie powiodło się", + "error.message.already.deleted": "Wiadomość została już usunięta", + "group.user.joined.by": "{0} dołączył do grupy", + "group.created.by": "Grupa utworzona przez {0}", + "group.user.left.by": "{0} opuścił grupę", + "action.group.fetch": "Pobieranie grup", + "group.created": "Utworzono grupę", + "group.user.joined": "Dołączono do grupy", + "prompt.invite.username": "Podaj nazwę użytkownika, aby zaprosić:", + "desc.failed.load.members": "Nie udało się załadować członków", + "prompt.are.you.sure": "Czy na pewno?", + "title.unknown.group": "Nieznana grupa", + "title.unknown": "Nieznany", + "key.rotated": "Klucze grupy zostały pomyślnie zrotowane.", + "success:banner.updated": "Baner został pomyślnie zaktualizowany", + "success:profile.updated": "Profil został pomyślnie zaktualizowany", + "success:avatar.updated": "Awatar został pomyślnie zaktualizowany", + "en-us": "Angielski", + "pl-pl": "Polski", + "en-cat": "Koci (Meowlish)" +} diff --git a/webroot/index.html b/webroot/index.html index e6361f63..c7570624 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -119,6 +119,25 @@
+ + @@ -208,8 +227,59 @@ } } - async function createGroup() { + async function refreshGroups(waittime = 0, showLoading = true) { + try { + if (showLoading) showAction("action.group.fetch", "grouprefresh"); + await delay(waittime); + if (window.cachedGroups && typeof renderGroups === 'function') { + await renderGroups(window.cachedGroups); + } + let res = await fetchEncrypted("user/group/list"); + window.cachedGroups = res; + if (typeof renderGroups === 'function') { + await renderGroups(res); + } + if (showLoading) clearAction("grouprefresh"); + } + catch (e) { + if (showLoading) clearAction("grouprefresh"); + showBlahNotification("error:group.refresh.failed"); + } + } + async function createGroup() { + try { + let groupName = document.getElementById("creategroup-name").value; + if (!groupName || groupName.trim() === "") { + showBlahNotification("error:group.name.empty"); + return; + } + + showAction("action.group.creating", "group.action"); + + await ensureUserKeys(); + const roomKey = randomRoomKey256Chars(); + const selfWrapped = await wrapRoomKeyForSelf(roomKey); + + let payload = JSON.stringify({ + string1: groupName, + string2: selfWrapped + }); + + let res = await fetchEncrypted("group/create", payload); + clearAction("group.action"); + + if (res.startsWith("success:")) { + showBlahNotification("success:group.created"); + await refreshGroups(200, false); + } else { + showBlahNotification(res || "error:something.wrong"); + } + } catch (e) { + clearAction("group.action"); + showBlahNotification("error:something.wrong"); + console.error(e); + } } diff --git a/webroot/main.js b/webroot/main.js index 1ae4b4b8..82922c97 100644 --- a/webroot/main.js +++ b/webroot/main.js @@ -367,6 +367,171 @@ async function ensureDmRoomKey(dmId) { window.ensureDmRoomKey = ensureDmRoomKey; +window.groupKeysCache = {}; + +async function getGroupKey(groupId, keyIndex) { + if (!window.groupKeysCache[groupId]) { + window.groupKeysCache[groupId] = {}; + } + if (window.groupKeysCache[groupId][keyIndex]) { + return window.groupKeysCache[groupId][keyIndex]; + } + + let payload = JSON.stringify({string1: groupId, string2: keyIndex.toString()}); + let res = await fetchEncrypted("group/key/get", payload); + if (!res || res.startsWith("error:")) { + return null; + } + + try { + let decryptedKey; + if (res.startsWith("SETUP:")) { + const rest = res.substring("SETUP:".length); + const parts = rest.split(";"); + if (parts.length < 2) throw new Error("error:group.key.setup.malformed"); + + const otherPublicJson = parts[0]; + const setupPayloadJson = parts.slice(1).join(";"); + const otherPublic = JSON.parse(otherPublicJson); + const setupPayload = JSON.parse(setupPayloadJson); + + const aesKeyBytes = await deriveHybridKeyFromSetup(otherPublic, setupPayload.ctMlKem); + decryptedKey = await decryptAesGcmFromBase64(setupPayload.enc, aesKeyBytes); + } else { + decryptedKey = await unwrapRoomKeyForSelf(res); + } + + window.groupKeysCache[groupId][keyIndex] = decryptedKey; + return decryptedKey; + } catch(e) { + return null; + } +} + +function closeRotationPopup() { + let popupContainer = document.getElementById("rotation-popup-container"); + if(popupContainer) { + popupContainer.classList.remove("show"); + setTimeout(() => { + popupContainer.style.display = "none"; + }, 200); + } + clearAction("key_rotation"); +} + +let pendingRotationMessagePayload = null; + +async function showRotationPopup(groupId, originalMessagePayload) { + pendingRotationMessagePayload = originalMessagePayload; + showAction("action.key.rotation", "key_rotation"); + + try { + let membersRaw = await fetchEncrypted("group/members", groupId); + if(membersRaw && membersRaw.startsWith("error:")) throw new Error(membersRaw); + + let members = JSON.parse(membersRaw); + + let desc = document.getElementById("rotation-popup-desc"); + desc.innerHTML = `desc.key.rotation.info

${members.length} label.members`; + let blahTags = desc.getElementsByTagName("blah"); + for (let i = 0; i < blahTags.length; i++) { + blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML); + } + + let popupContainer = document.getElementById("rotation-popup-container"); + popupContainer.style.display = "flex"; + popupContainer.offsetHeight; // trigger reflow + popupContainer.classList.add("show"); + } catch(e) { + clearAction("key_rotation"); + showBlahNotification("error:something.wrong"); + } +} + +async function executeKeyRotation() { + closeRotationPopup(); + showAction("action.key.rotating", "key_rotation_exec"); + + try { + let groupId = currentDmId; + + let membersRaw = await fetchEncrypted("group/members", groupId); + if(membersRaw && membersRaw.startsWith("error:")) throw new Error(membersRaw); + let members = JSON.parse(membersRaw); + + let roomKey = randomRoomKey256Chars(); + let keysDict = {}; + let myPublicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${window.id}`); + + for(let memberId of members) { + let pureId = memberId.split(";")[0]; + if(pureId === window.id) { + keysDict[memberId] = await wrapRoomKeyForSelf(roomKey); + } else { + let publicKeysRaw = await fetchAsync(`${url}/user/key/getpublic?id=${pureId}`); + if(publicKeysRaw && publicKeysRaw.length > 10) { + let publicKeys = JSON.parse(publicKeysRaw); + let hybrid = await deriveHybridKeyToUser(publicKeys); + let encForMember = await encryptAesGcmToBase64(roomKey, hybrid.aesKeyBytes); + let setupPayloadJson = JSON.stringify({ + v: 1, + alg: "hybrid-x25519-mlkem768", + ctMlKem: hybrid.ciphertextMlKemBase64, + enc: encForMember + }); + keysDict[memberId] = `SETUP:${myPublicKeysRaw};${setupPayloadJson}`; + } else { + throw new Error("error:missing.keys"); + } + } + } + + let payload = JSON.stringify({ + groupId: groupId, + keys: keysDict + }); + + let res = await fetchEncrypted("group/key/rotate", payload); + if(res && res.startsWith("error:")) { + throw new Error(res); + } + + // Success! + let newIndex = res.replace("success:", ""); + window.currentGroupKeyIndex = newIndex; + + if(!window.groupKeysCache[groupId]) window.groupKeysCache[groupId] = {}; + window.groupKeysCache[groupId][newIndex] = roomKey; + + clearAction("key_rotation_exec"); + showBlahNotification("success:key.rotated"); + + // Retry sending the message! + if(pendingRotationMessagePayload) { + let p = pendingRotationMessagePayload; + p.string5 = newIndex; + let dmKeyBytes = await sha256Bytes(base64ToUint8(roomKey)); + p.string2 = await encryptAesGcmToBase64(p.rawPlainContent, dmKeyBytes); // we need to save rawPlainContent for retry + + showAction("action.message.sending", "msgsending"); + let resSend = await fetchEncrypted("group/message/send", JSON.stringify(p)); + clearAction("msgsending"); + + if(resSend && resSend.startsWith("error:")) { + showBlahNotification(resSend); + } else if(resSend && resSend.startsWith("success:")) { + loadDmMessages(currentDmId); + } + } + pendingRotationMessagePayload = null; + + } catch(e) { + clearAction("key_rotation_exec"); + console.error(e); + showBlahNotification(e.message && e.message.startsWith("error:") ? e.message : "error:something.wrong"); + } +} + async function encrypt(plainText, keyBytes) { const iv = window.crypto.getRandomValues(new Uint8Array(16)); const encoder = new TextEncoder(); @@ -857,6 +1022,18 @@ async function getAvatarUrl(id, username) { } } +async function getGroupAvatarUrl(groupId, groupName) { + try { + let pfpUrl = `${url}/group/storage/public/getentry?id=${groupId}&e=larp.profile.pfp`; + if ((await fetchAsync(pfpUrl)) == "") { + throw Error(); + } + return pfpUrl; + } catch (e) { + return createAvatarSvg(groupName); + } +} + function updateLoadingStatus(message) { loadingStatus.innerHTML = processBlah(message); } @@ -1743,6 +1920,71 @@ function setActiveRoombarItem(itemId) { } } +async function acceptGroupInvite(groupId, inviterId) { + try { + showAction("action.invite.accepting", "invite.action"); + + let payload = JSON.stringify({ + string1: groupId, + string2: inviterId + }); + + let res = await fetchEncrypted("group/join", payload); + clearAction("invite.action"); + showBlahNotification(res || "success:joined"); + + // Refresh invites and groups + window.cachedInvites = {'received': null, 'sent': null}; + loadInvites("received"); + if (typeof refreshGroups === 'function') refreshGroups(0, false); + } catch (e) { + clearAction("invite.action"); + showBlahNotification("error:something.wrong"); + } +} + +async function declineGroupInvite(groupId, inviterId) { + try { + showAction("action.invite.declining", "invite.action"); + + let payload = JSON.stringify({ + string1: groupId, + string2: inviterId + }); + + let res = await fetchEncrypted("group/invite/decline", payload); + clearAction("invite.action"); + showBlahNotification(res || "success:invite.declined"); + + window.cachedInvites = {'received': null, 'sent': null}; + loadInvites("received"); + } catch (e) { + clearAction("invite.action"); + showBlahNotification("error:something.wrong"); + } +} + +async function revokeGroupInvite(groupId, targetId) { + try { + showAction("action.invite.revoking", "invite.action"); + + let payload = JSON.stringify({ + string1: groupId, + string2: targetId + }); + + let res = await fetchEncrypted("group/invite/revoke", payload); + clearAction("invite.action"); + showBlahNotification(res || "success:invite.revoked"); + + window.cachedInvites = {'received': null, 'sent': null}; + loadInvites("sent"); + } catch (e) { + clearAction("invite.action"); + showBlahNotification("error:something.wrong"); + } +} + async function renderInvites(res, type) { let container = document.getElementById("invites-container"); if (!container) return; @@ -1781,7 +2023,7 @@ async function renderInvites(res, type) { } let onlyId = id.split(":")[0]; let username = await fetchAsync(`${url}/idtoname?id=${id}`); - if (!(username && username.trim() !== "")) return; + if (!(username && username.trim() !== "")) continue; count++; let pfp = await getAvatarUrl(id, username); @@ -1789,8 +2031,9 @@ async function renderInvites(res, type) { let actions = ""; let desc = ""; + let displayName = username.split(':')[0] || onlyId; if (type === "received") { - desc = `:desc.invite.dm.received:${username};${onlyId}`; + desc = `:desc.invite.dm.received:${displayName};${onlyId}`; actions = ` `; } else { - desc = `:desc.invite.dm.sent:${username};${onlyId}`; + desc = `:desc.invite.dm.sent:${displayName};${onlyId}`; actions = ` + + `; + } else { + desc = `:desc.invite.group.sent:${val1};${val2}`; + actions = ` + + `; + } + + let entry = invitesEntry + .replaceAll("{username}", groupName) + .replaceAll("{pfp}", pfp) + .replaceAll("{desc}", `${desc}`) + .replaceAll("{actions}", actions) + .replaceAll("{onclick_attr}", "") + .replaceAll("{id}", groupId); + html += entry; } } @@ -1996,7 +2297,10 @@ async function gotoHome(splash = true) { switchRoomsBar("title.home", homeRoomBar); setActiveRoombarItem(null); setupWebSocket(); - await refreshDms(210); + await Promise.all([ + refreshDms(210), + (typeof refreshGroups === 'function' ? refreshGroups(210) : Promise.resolve()) + ]); } function setActiveSidebarIndicator(element, isTemporary = false) { @@ -2210,6 +2514,67 @@ async function renderDms(res) { } } +async function renderGroups(res) { + let container = document.getElementById("groups-list"); + if (!container) return; + + if (res && res !== "") { + let parsed = JSON.parse(res); + let groups = parsed.groups || {}; + let groupEntries = Object.entries(groups); + + //Sort by timestamp descending + groupEntries.sort((a, b) => (parseInt(b[1].string2) || 0) - (parseInt(a[1].string2) || 0)); + + let html = ""; + let count = 0; + + for (let [groupId, groupData] of groupEntries) { + let isRead = groupData.string3 === "true"; + let nameStyle = isRead ? "opacity: 0.8;" : "opacity: 1;"; + + let infoRes = await fetchEncrypted("group/info", groupId); + if (infoRes && infoRes.startsWith("{")) { + let info = JSON.parse(infoRes); + let groupName = info.string2; + + let safeGroupName = encodeURIComponent(groupName).replace(/'/g, "%27"); + let pfp = await getGroupAvatarUrl(groupId, groupName); + + html += ` + + `; + count++; + } + } + + if (count > 0) { + container.innerHTML = html; + container.classList.remove("empty"); + } else { + container.innerHTML = `

desc.no.groups

`; + container.classList.add("empty"); + let blahTags = container.getElementsByTagName("blah"); + for (let i = 0; i < blahTags.length; i++) { + blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML); + } + } + } else { + container.innerHTML = `

desc.no.groups

`; + container.classList.add("empty"); + let blahTags = container.getElementsByTagName("blah"); + for (let i = 0; i < blahTags.length; i++) { + blahTags[i].innerHTML = processBlah(blahTags[i].innerHTML); + } + } +} + var currentDmId = null; var currentDmKey = null; // AES key for encrypting/decrypting messages var dmUsernameCache = {}; @@ -2263,30 +2628,131 @@ async function generateDmMembersHtml(targetId, targetUsername) { `; } -async function openDm(dmId, username, targetId) { +async function inviteToGroupPrompt(groupId) { + let targetUsername = prompt(processBlah("prompt.invite.username")); + if (!targetUsername) return; + + if (!targetUsername.includes(":")) { + targetUsername += `:${host}`; + } + + let idRes = await fetchAsync(`${url}/nametoid?u=${targetUsername}`); + if (idRes.startsWith("error") || idRes.trim() === "0" || idRes.trim() === "") { + showBlahNotification("error:user.not.found"); + return; + } + let targetId = idRes.trim(); + + try { + showAction("action.invite.sending", "invite.action"); + + let payload = JSON.stringify({ + string1: groupId, + string2: targetId + }); + + let res = await fetchEncrypted("group/invite", payload); + clearAction("invite.action"); + showBlahNotification(res || "success:invite.sent"); + } catch (e) { + clearAction("invite.action"); + showBlahNotification("error:something.wrong"); + } +} + +async function generateGroupMembersHtml(groupId, groupName) { + let membersJson = await fetchEncrypted("group/members", groupId); + if (!membersJson || membersJson.startsWith("error")) { + return `
desc.failed.load.members
`; + } + + let members = JSON.parse(membersJson); + let html = ` +
+ `; + + for (let memberId of members) { + let memberUsername = await fetchAsync(`${url}/idtoname?id=${memberId}`); + if (!memberUsername || memberUsername.startsWith("error")) continue; + + let memberPfp = await getAvatarUrl(memberId, memberUsername); + let memberDisplayName = memberUsername.split(':')[0]; + + html += ` + `; + } + + html += `
`; + return html; +} + +async function openDm(dmId, username, targetId, type = 'dms') { try { showAction("action.dm.opening", "dmopen"); - markDmAsRead(dmId); + if (type === 'groups') { + markDmAsRead(dmId, 'groups'); + } else { + markDmAsRead(dmId, 'dms'); + } - currentDmKey = await ensureDmRoomKey(dmId); - if (!currentDmKey) { - throw new Error("Missing DM room key"); + if (type === 'dms') { + currentDmKey = await ensureDmRoomKey(dmId); + if (!currentDmKey) { + throw new Error("Missing DM room key"); + } + } else { + let infoPayload = await fetchEncrypted("group/info", dmId); + if (infoPayload && !infoPayload.startsWith("error:")) { + let info = JSON.parse(infoPayload); + window.currentGroupKeyIndex = info.string4 || "0"; + } else { + window.currentGroupKeyIndex = "0"; + } + } + + if (currentDmId !== dmId) { + currentRoomContentTitle = ""; } currentDmId = dmId; + window.currentRoomType = type; // Save the type globally loadedMessages = {}; oldestLoadedMsgId = null; isLoadingOlderMessages = false; - let pfp = await getAvatarUrl(targetId, username); - let iconHtml = ``; + let iconHtml = ""; + if (type === 'dms') { + let pfp = await getAvatarUrl(targetId, username); + iconHtml = ``; + } else { + let pfp = await getGroupAvatarUrl(dmId, username); + iconHtml = ``; + } if (currentRoomsBarTitle !== "title.home") { gotoHome(false); } await switchRoomContent(username.split(':')[0], chatScreen, true, iconHtml); - let membersHtml = await generateDmMembersHtml(targetId, username); + + let membersHtml = ""; + if (type === 'dms') { + membersHtml = await generateDmMembersHtml(targetId, username); + } else { + membersHtml = await generateGroupMembersHtml(dmId, username); + } await switchDetailsContent("title.details", membersHtml); let msgContainer = document.getElementById("chat-messages"); @@ -2326,7 +2792,8 @@ async function openDm(dmId, username, targetId) { async function loadDmMessages(dmId, startOffset = "", isPrepend = false) { try { let payload = JSON.stringify({string1: dmId, string2: "50", string3: startOffset}); - let res = await fetchEncrypted("dm/messages/get", payload); + let endpoint = window.currentRoomType === 'groups' ? "group/messages/get" : "dm/messages/get"; + let res = await fetchEncrypted(endpoint, payload); if (res && res.startsWith("error:")) { showBlahNotification(res); @@ -2389,9 +2856,26 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { let decrypted = false; if (msg.key && msg.key !== "") { try { - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); - content = await decryptAesGcmFromBase64(content, dmKeyBytes); - decrypted = true; + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + keyToUse = await getGroupKey(currentDmId, msg.key); + if (keyToUse) { + let mK = parseInt(msg.key); + let cK = parseInt(window.currentGroupKeyIndex || "0"); + if (!isNaN(mK) && !isNaN(cK) && mK > cK) { + window.currentGroupKeyIndex = msg.key; + } + } + } + + if (!keyToUse) { + content = `error:messages.decrypt.failed`; + decrypted = false; + } else { + let keyBytes = await sha256Bytes(base64ToUint8(keyToUse)); + content = await decryptAesGcmFromBase64(content, keyBytes); + decrypted = true; + } } catch (e) { content = `error:messages.decrypt.failed`; decrypted = false; @@ -2423,7 +2907,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { let safeType = encodeURIComponent(att.type).replace(/'/g, "%27"); let safeName = encodeURIComponent(att.name).replace(/'/g, "%27"); - attachmentsHtml += `
+ attachmentsHtml += `
${iconSvg}
${escapeHtml(displayName)}
@@ -2454,8 +2938,8 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { content = processBlah(content); if (msg.type && msg.type.startsWith("larp.info")) { - if (msg.type === "larp.info.pin" || msg.type === "larp.info.unpin") { - let authorName = "Unknown"; + if (msg.type === "larp.info.pin" || msg.type === "larp.info.unpin" || msg.type === "larp.info.join" || msg.type === "larp.info.leave" || msg.type === "larp.info.create") { + let authorName = processBlah("title.unknown"); if (msg.author && msg.author !== "0") { if (!dmUsernameCache[msg.author]) { let fullUsername = await fetchAsync(`${url}/idtoname?id=${msg.author}`); @@ -2471,14 +2955,22 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { } } - let isPinned = msg.type === "larp.info.pin"; - let actionStr = isPinned ? "message.pinned.by" : "message.unpinned.by"; + let actionStr = ""; + if (msg.type === "larp.info.pin") actionStr = "message.pinned.by"; + else if (msg.type === "larp.info.unpin") actionStr = "message.unpinned.by"; + else if (msg.type === "larp.info.join") actionStr = "group.user.joined.by"; + else if (msg.type === "larp.info.leave") actionStr = "group.user.left.by"; + else if (msg.type === "larp.info.create") actionStr = "group.created.by"; - let clickableMsg = `${processBlah('title.message.lowercase')}`; let clickableAuthor = `${authorName}`; let textStr = processBlah(actionStr); - content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg); + if (msg.type.endsWith(".pin") || msg.type.endsWith(".unpin")) { + let clickableMsg = `${processBlah('title.message.lowercase')}`; + content = textStr.replace("{0}", clickableAuthor).replace("{1}", clickableMsg); + } else { + content = textStr.replace("{0}", clickableAuthor); + } } html += `
${content}
`; lastAuthor = null; @@ -2501,7 +2993,7 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { } lastAuthor = msg.author; - let authorName = "Unknown"; + let authorName = processBlah("title.unknown"); let pfp = "assets/default_avatar.png"; if (msg.author !== "0") { if (!dmUsernameCache[msg.author]) { @@ -2618,8 +3110,15 @@ async function generateMessagesHtml(entries, isPinnedContext = false) { let rxVal = rx.r; if (msg.key && msg.key !== "") { try { - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); - rxVal = await decryptAesGcmFromBase64(rxVal, dmKeyBytes); + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + let k = await getGroupKey(currentDmId, msg.key); + if (k) keyToUse = k; + } + if (keyToUse) { + let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse)); + rxVal = await decryptAesGcmFromBase64(rxVal, dmKeyBytes); + } } catch(e) {} } if (!rxCounts[rxVal]) rxCounts[rxVal] = {count: 0, me: false, myReactionId: null}; @@ -2839,8 +3338,8 @@ function setupChatScrollListener() { -async function downloadAndRenderAttachment(container, msgId, attId, type, name) { - if (!currentDmId || !currentDmKey) return; +async function downloadAndRenderAttachment(container, msgId, attId, type, name, keyIndex = "0") { + if (!currentDmId || (window.currentRoomType === 'dms' && !currentDmKey)) return; if (!container) return; if (container.dataset.loaded === "true") { @@ -2860,10 +3359,16 @@ async function downloadAndRenderAttachment(container, msgId, attId, type, name) container.innerHTML = `
${ICONS.spinner()}
`; try { - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + let k = await getGroupKey(currentDmId, keyIndex); + if(k) keyToUse = k; + } + let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse)); let nonce = await getNonce(id, passwordHash); - let res = await fetch(`${url}/dm/attachment/get?id=${id}&dm=${currentDmId}&msg=${msgId}&att=${attId}`, { + let endpointType = window.currentRoomType === 'groups' ? "group" : "dm"; + let res = await fetch(`${url}/${endpointType}/attachment/get?id=${id}&dm=${currentDmId}&msg=${msgId}&att=${attId}`, { headers: { "secret": await encryptWithNonce(passwordHash, passwordHash, nonce) } @@ -2953,7 +3458,7 @@ function renderAttachmentsPreview() { } async function sendMessage(sendbutton = document.getElementById("chat-sendmessage")) { - if (!currentDmId || !currentDmKey) return; + if (!currentDmId || (window.currentRoomType === 'dms' && !currentDmKey)) return; let input = document.getElementById("chat-input"); let content = input.value.trim(); @@ -2975,7 +3480,13 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag } try { - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + let k = await getGroupKey(currentDmId, window.currentGroupKeyIndex || "0"); + if(k) keyToUse = k; + } + + let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse)); let encryptedContent = await encryptAesGcmToBase64(content, dmKeyBytes); let attachmentsPayload = []; @@ -3035,6 +3546,11 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag string3: attachmentsPayload.length > 0 ? JSON.stringify(attachmentsPayload) : "", string4: replyingToMsgId || "" }; + + if (window.currentRoomType === 'groups') { + msgPayload.string5 = window.currentGroupKeyIndex || "0"; + msgPayload.rawPlainContent = content; // for retry + } input.value = ""; input.style.height = '2.5rem'; @@ -3049,9 +3565,14 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag showAction("action.message.sending", "msgsending"); - let res = await fetchEncrypted("dm/message/send", JSON.stringify(msgPayload)); + let res = await fetchEncrypted(window.currentRoomType === 'groups' ? "group/message/send" : "dm/message/send", JSON.stringify(msgPayload)); clearAction("msgsending"); - if (res && res.startsWith("error:")) { + + if (res && res.startsWith("error:key.rotation.required") && window.currentRoomType === 'groups') { + let pElem = document.getElementById(pendingMsgId); + if (pElem) pElem.remove(); + showRotationPopup(currentDmId, msgPayload); + } else if (res && res.startsWith("error:")) { showBlahNotification(res); let pElem = document.getElementById(pendingMsgId); if (pElem) pElem.remove(); @@ -3073,7 +3594,7 @@ async function sendMessage(sendbutton = document.getElementById("chat-sendmessag type: "larp.text", content: encryptedContent, attachment: finalAttPayload, - key: "0", + key: window.currentRoomType === 'groups' ? (window.currentGroupKeyIndex || "0") : "0", pervious: "", responded: replyingToMsgId || "", reactions: "" @@ -3148,15 +3669,16 @@ function setupWebSocket() { appWebSocket.onmessage = (event) => { let data = event.data; - if (data.startsWith("dm_message:")) { + if (data.startsWith("dm_message:") || data.startsWith("group_message:")) { let parts = data.split(":"); + let msgTypeRoom = data.startsWith("dm_message:") ? "dms" : "groups"; let msgDmId = parts[1]; let msgType = parts[2] || "unread"; if (msgType === "unread") { - updateDmListUI(msgDmId, false, Date.now().toString()); + updateDmListUI(msgDmId, false, msgTypeRoom, Date.now().toString()); } else if (msgType === "read") { - updateDmListUI(msgDmId, true, Date.now().toString()); + updateDmListUI(msgDmId, true, msgTypeRoom, Date.now().toString()); } else if (msgType === "update") { // do nothing to UI sorting/read state } @@ -3246,7 +3768,8 @@ async function togglePinMessage(msgId) { string1: currentDmId, string2: msgId }); - await fetchEncrypted("dm/message/pin/toggle", payload); + let endpoint = window.currentRoomType === 'groups' ? "group/message/pin/toggle" : "dm/message/pin/toggle"; + await fetchEncrypted(endpoint, payload); } catch (e) { console.error(e); showBlahNotification("error.network.error"); @@ -3270,7 +3793,8 @@ async function deleteMessage(msgId) { string1: currentDmId, string2: msgId }; - let res = await fetchEncrypted("dm/message/delete", JSON.stringify(msgPayload)); + let endpoint = window.currentRoomType === 'groups' ? "group/message/delete" : "dm/message/delete"; + let res = await fetchEncrypted(endpoint, JSON.stringify(msgPayload)); if (res && res.startsWith("error:")) { showBlahNotification(res); } @@ -3346,8 +3870,15 @@ async function replyMessage(msgId) { let content = msg ? msg.content : ""; if (msg && msg.key && msg.key !== "") { try { - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); - content = await decryptAesGcmFromBase64(content, dmKeyBytes); + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + let k = await getGroupKey(currentDmId, msg.key); + if(k) keyToUse = k; + } + if (keyToUse) { + let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse)); + content = await decryptAesGcmFromBase64(content, dmKeyBytes); + } } catch (e) { } } @@ -3454,8 +3985,15 @@ async function reactMessagePrompt(msgId, quickReaction = null) { let rxVal = rx.r; if (currentMsg.key && currentMsg.key !== "") { try { - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); - rxVal = await decryptAesGcmFromBase64(rxVal, dmKeyBytes); + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + let k = await getGroupKey(currentDmId, currentMsg.key); + if(k) keyToUse = k; + } + if (keyToUse) { + let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse)); + rxVal = await decryptAesGcmFromBase64(rxVal, dmKeyBytes); + } } catch(e) {} } if (rx.u === id && rxVal === reaction) { @@ -3468,10 +4006,17 @@ async function reactMessagePrompt(msgId, quickReaction = null) { } let isAdding = (myReactionId === null); - let dmKeyBytes = await sha256Bytes(base64ToUint8(currentDmKey)); + let keyToUse = currentDmKey; + if (window.currentRoomType === 'groups') { + let k = await getGroupKey(currentDmId, currentMsg.key); + if(k) keyToUse = k; + } + let dmKeyBytes = await sha256Bytes(base64ToUint8(keyToUse)); let encryptedReaction = await encryptAesGcmToBase64(reaction, dmKeyBytes); - let endpoint = isAdding ? "dm/message/react/add" : "dm/message/react/remove"; + let endpoint = window.currentRoomType === 'groups' + ? (isAdding ? "group/message/react/add" : "group/message/react/remove") + : (isAdding ? "dm/message/react/add" : "dm/message/react/remove"); let msgPayload = { string1: currentDmId, string2: msgId, @@ -3615,7 +4160,7 @@ async function changePassword() { } async function deleteAccount() { - if (!confirm(processBlah("desc.delete.account") + "\n\nAre you sure?")) return; + if (!confirm(processBlah("desc.delete.account") + "\n\n" + processBlah("prompt.are.you.sure"))) return; showAction("action.account.deleting", "accountdel"); let res = await fetchPostEnc(`${url}/deleteaccount?id=${window.id}`, ""); clearAction("accountdel"); @@ -3978,48 +4523,87 @@ if (container) { document.addEventListener('touchend', endDrag); } -function updateDmListUI(dmId, isRead, timestamp = null) { +function updateDmListUI(roomId, isRead, type = 'dms', timestamp = null) { try { - if (!window.cachedDms) return; - let parsedDms = JSON.parse(window.cachedDms); - if (!parsedDms.dms || !parsedDms.dms[dmId]) return; - - let changed = false; - if (parsedDms.dms[dmId].string3 !== (isRead ? "true" : "false")) { - parsedDms.dms[dmId].string3 = isRead ? "true" : "false"; - changed = true; - } - if (timestamp && parsedDms.dms[dmId].string2 !== timestamp) { - parsedDms.dms[dmId].string2 = timestamp; - changed = true; - } - - if (changed) { - window.cachedDms = JSON.stringify(parsedDms); - } - - let btn = document.getElementById(`dm-btn-${dmId}`); - let container = document.getElementById("dms-list"); - if (btn && container) { - let nameSpan = btn.querySelector('.room-name'); - if (nameSpan) { - nameSpan.style.opacity = isRead ? "0.8" : "1"; + if (type === 'groups') { + if (!window.cachedGroups) return; + let parsedGroups = JSON.parse(window.cachedGroups); + if (!parsedGroups.groups || !parsedGroups.groups[roomId]) return; + + let changed = false; + if (parsedGroups.groups[roomId].string3 !== (isRead ? "true" : "false")) { + parsedGroups.groups[roomId].string3 = isRead ? "true" : "false"; + changed = true; } - if (timestamp) { - container.prepend(btn); + if (timestamp && parsedGroups.groups[roomId].string2 !== timestamp) { + parsedGroups.groups[roomId].string2 = timestamp; + changed = true; + } + + if (changed) { + window.cachedGroups = JSON.stringify(parsedGroups); + } + + let btn = document.getElementById(`group-btn-${roomId}`); + let container = document.getElementById("groups-list"); + if (btn && container) { + let nameSpan = btn.querySelector('.room-name'); + if (nameSpan) { + nameSpan.style.opacity = isRead ? "0.8" : "1"; + } + if (timestamp) { + container.prepend(btn); + } + } else { + if (typeof refreshGroups === 'function') refreshGroups(0, false); } } else { - if (typeof refreshDms === 'function') refreshDms(0, false); + if (!window.cachedDms) return; + let parsedDms = JSON.parse(window.cachedDms); + if (!parsedDms.dms || !parsedDms.dms[roomId]) return; + + let changed = false; + if (parsedDms.dms[roomId].string3 !== (isRead ? "true" : "false")) { + parsedDms.dms[roomId].string3 = isRead ? "true" : "false"; + changed = true; + } + if (timestamp && parsedDms.dms[roomId].string2 !== timestamp) { + parsedDms.dms[roomId].string2 = timestamp; + changed = true; + } + + if (changed) { + window.cachedDms = JSON.stringify(parsedDms); + } + + let btn = document.getElementById(`dm-btn-${roomId}`); + let container = document.getElementById("dms-list"); + if (btn && container) { + let nameSpan = btn.querySelector('.room-name'); + if (nameSpan) { + nameSpan.style.opacity = isRead ? "0.8" : "1"; + } + if (timestamp) { + container.prepend(btn); + } + } else { + if (typeof refreshDms === 'function') refreshDms(0, false); + } } } catch (e) { console.error(e); if (typeof refreshDms === 'function') refreshDms(0, false); + if (typeof refreshGroups === 'function') refreshGroups(0, false); } } -function markDmAsRead(dmId) { - fetchEncrypted("dm/read", JSON.stringify({string1: dmId})).catch(()=>{}); - updateDmListUI(dmId, true); +function markDmAsRead(roomId, type = 'dms') { + if (type === 'groups') { + fetchEncrypted("group/read", JSON.stringify({string1: roomId})).catch(()=>{}); + } else { + fetchEncrypted("dm/read", JSON.stringify({string1: roomId})).catch(()=>{}); + } + updateDmListUI(roomId, true, type); } function handleDmInteraction() { @@ -4029,12 +4613,21 @@ function handleDmInteraction() { if (lastChatScroll <= 10) { try { - if (window.cachedDms) { - let parsedDms = JSON.parse(window.cachedDms); - if (parsedDms.dms && parsedDms.dms[currentDmId] && parsedDms.dms[currentDmId].string3 !== "true") { - markDmAsRead(currentDmId); + if (window.currentRoomType === 'groups') { + if (window.cachedGroups) { + let parsedGroups = JSON.parse(window.cachedGroups); + if (parsedGroups.groups && parsedGroups.groups[currentDmId] && parsedGroups.groups[currentDmId].string3 !== "true") { + markDmAsRead(currentDmId, window.currentRoomType); + } + } + } else { + if (window.cachedDms) { + let parsedDms = JSON.parse(window.cachedDms); + if (parsedDms.dms && parsedDms.dms[currentDmId] && parsedDms.dms[currentDmId].string3 !== "true") { + markDmAsRead(currentDmId, window.currentRoomType); + } + } } - } } catch (e) {} } } @@ -4056,7 +4649,8 @@ async function showPinnedMessages(forceReload = false, showPanel = true) { } try { - let res = await fetchEncrypted("dm/messages/pinned", currentDmId); + let endpoint = window.currentRoomType === 'groups' ? "group/messages/pinned" : "dm/messages/pinned"; + let res = await fetchEncrypted(endpoint, currentDmId); if (res && !res.startsWith("error:")) { let msgs = JSON.parse(res); let entries = Object.entries(msgs).sort((a, b) => parseInt(a[0]) - parseInt(b[0])); diff --git a/webroot/screens.js b/webroot/screens.js index 84e3d765..ead665d2 100644 --- a/webroot/screens.js +++ b/webroot/screens.js @@ -117,10 +117,10 @@ var notifisScreen = ` var invitesEntry = `
- +
- {username} - {desc} + {username} + {desc}
diff --git a/webroot/style.css b/webroot/style.css index 84660d6f..58e48f25 100644 --- a/webroot/style.css +++ b/webroot/style.css @@ -215,6 +215,25 @@ sidebar { width: calc(var(--icon-button-height) + (var(--button-margin) * 2) + var(--border-width)); border-right: var(--border-width) solid var(--light-border-color); } + +sidebar:not(.second) { + overflow-y: auto; + overflow-x: hidden; +} + +sidebar:not(.second)::-webkit-scrollbar { + display: none; +} +sidebar:not(.second) { + -ms-overflow-style: none; + scrollbar-width: none; +} + +#roomsbar roomcontent2 { + overflow-y: auto; + overflow-x: hidden; +} + sidebar.second { width: 17rem; min-width: 17rem;