friend and more update

This commit is contained in:
MathMan05 2024-11-26 22:30:04 -06:00
parent e24afe2abe
commit 2502d8f977
13 changed files with 491 additions and 79 deletions

View file

@ -40,6 +40,7 @@ jsc: {
gulp.task('watch', function () {
gulp.watch('./src', gulp.series("default"));
gulp.watch('./translations', gulp.series("default"));
}, {debounceDelay: 10});
// Clean task to delete the dist directory

View file

@ -842,6 +842,10 @@ class Channel extends SnowFlake{
if(this.voice&&localStorage.getItem("Voice enabled")){
this.localuser.joinVoice(this);
}
(document.getElementById("typebox") as HTMLDivElement).contentEditable =""+this.canMessage;
(document.getElementById("upload") as HTMLElement).style.visibility=this.canMessage?"visible":"hidden";
(document.getElementById("typediv") as HTMLElement).style.visibility="visible";
(document.getElementById("typebox") as HTMLDivElement).focus();
await this.putmessages();
await prom;
if(id !== Channel.genid){
@ -851,10 +855,7 @@ class Channel extends SnowFlake{
await this.buildmessages();
//loading.classList.remove("loading");
(document.getElementById("typebox") as HTMLDivElement).contentEditable =""+this.canMessage;
(document.getElementById("upload") as HTMLElement).style.visibility=this.canMessage?"visible":"hidden";
(document.getElementById("typediv") as HTMLElement).style.visibility="visible";
(document.getElementById("typebox") as HTMLDivElement).focus();
}
typingmap: Map<Member, number> = new Map();
async typingStart(typing: startTypingjson): Promise<void>{
@ -938,6 +939,7 @@ class Channel extends SnowFlake{
}
lastmessage: Message | undefined;
async putmessages(){
//TODO swap out with the WS op code
if(this.allthewayup){
return;
}

View file

@ -8,9 +8,11 @@ import{ Permissions }from"./permissions.js";
import{ SnowFlake }from"./snowflake.js";
import{ Contextmenu }from"./contextmenu.js";
import { I18n } from "./i18n.js";
import { Float, FormError } from "./settings.js";
class Direct extends Guild{
declare channelids: { [key: string]: Group };
channels: Group[];
getUnixTime(): number{
throw new Error("Do not call this for Direct, it does not make sense");
}
@ -26,7 +28,7 @@ class Direct extends Guild{
this.roles = [];
this.roleids = new Map();
this.prevchannel = undefined;
this.properties.name = "Direct Messages";
this.properties.name = I18n.getTranslation("DMs.name");
for(const thing of json){
const temp = new Group(thing, this);
this.channels.push(temp);
@ -60,7 +62,7 @@ class Direct extends Guild{
icon.classList.add("svgicon","svg-friends","space");
freindDiv.append(icon);
freindDiv.append("Friends");
freindDiv.append(I18n.getTranslation("friends.friends"));
ddiv.append(freindDiv);
freindDiv.onclick=()=>{
this.loadChannel(null);
@ -69,6 +71,217 @@ class Direct extends Guild{
ddiv.append(build);
return ddiv;
}
noChannel(addstate:boolean){
if(addstate){
history.pushState([this.id,undefined], "", "/channels/" + this.id);
}
this.localuser.pageTitle(I18n.getTranslation("friends.friendlist"));
const channelTopic = document.getElementById("channelTopic") as HTMLSpanElement;
channelTopic.removeAttribute("hidden");
channelTopic.textContent="";
const loading = document.getElementById("loadingdiv") as HTMLDivElement;
loading.classList.remove("loading");
this.localuser.getSidePannel();
const messages = document.getElementById("channelw") as HTMLDivElement;
for(const thing of Array.from(messages.getElementsByClassName("messagecontainer"))){
thing.remove();
}
const container=document.createElement("div");
container.classList.add("messagecontainer","flexttb","friendcontainer")
messages.append(container);
const checkVoid=()=>{
if(this.localuser.channelfocus!==undefined||this.localuser.lookingguild!==this){
this.localuser.relationshipsUpdate=()=>{};
}
}
function genuserstrip(user:User,icons:HTMLElement):HTMLElement{
const div=document.createElement("div");
div.classList.add("flexltr","liststyle");
user.bind(div);
div.append(user.buildpfp());
const userinfos=document.createElement("div");
userinfos.classList.add("flexttb");
const username=document.createElement("span");
username.textContent=user.name;
userinfos.append(username,user.getStatus());
div.append(userinfos);
User.contextmenu.bindContextmenu(div,user,undefined);
userinfos.style.flexGrow="1";
div.append(icons);
return div;
}
{
//TODO update on users coming online
const online=document.createElement("button");
online.textContent=I18n.getTranslation("friends.online");
channelTopic.append(online);
const genOnline=()=>{
this.localuser.relationshipsUpdate=genOnline;
checkVoid();
container.innerHTML="";
container.append(I18n.getTranslation("friends.online:"));
for(const user of this.localuser.inrelation){
if(user.relationshipType===1&&user.online){
const buttonc=document.createElement("div");
const button1=document.createElement("span");
button1.classList.add("svg-frmessage","svgicon");
buttonc.append(button1);
buttonc.classList.add("friendlyButton");
buttonc.onclick=(e)=>{
e.stopImmediatePropagation();
user.opendm();
}
container.append(genuserstrip(user,buttonc));
}
}
}
online.onclick=genOnline;
genOnline();
}
{
const all=document.createElement("button");
all.textContent=I18n.getTranslation("friends.all");
const genAll=()=>{
this.localuser.relationshipsUpdate=genAll;
checkVoid();
container.innerHTML="";
container.append(I18n.getTranslation("friends.all:"));
for(const user of this.localuser.inrelation){
if(user.relationshipType===1){
const buttonc=document.createElement("div");
const button1=document.createElement("span");
button1.classList.add("svg-frmessage","svgicon");
buttonc.append(button1);
buttonc.classList.add("friendlyButton");
buttonc.onclick=(e)=>{
e.stopImmediatePropagation();
user.opendm();
}
container.append(genuserstrip(user,buttonc));
}
}
}
all.onclick=genAll;
channelTopic.append(all);
}
{
const pending=document.createElement("button");
pending.textContent=I18n.getTranslation("friends.pending");
const genPending=()=>{
this.localuser.relationshipsUpdate=genPending;
checkVoid();
container.innerHTML="";
container.append(I18n.getTranslation("friends.pending:"));
for(const user of this.localuser.inrelation){
if(user.relationshipType===3||user.relationshipType===4){
const buttons=document.createElement("div");
buttons.classList.add("flexltr");
const buttonc=document.createElement("div");
const button1=document.createElement("span");
button1.classList.add("svgicon","svg-x");
if(user.relationshipType===3){
const buttonc=document.createElement("div");
const button2=document.createElement("span");
button2.classList.add("svgicon","svg-x");
button2.classList.add("svg-addfriend");
buttonc.append(button2);
buttonc.classList.add("friendlyButton");
buttonc.append(button2);
buttons.append(buttonc);
buttonc.onclick=(e)=>{
e.stopImmediatePropagation();
user.changeRelationship(1);
outerDiv.remove();
}
}
buttonc.append(button1);
buttonc.classList.add("friendlyButton");
buttonc.onclick=(e)=>{
e.stopImmediatePropagation();
user.changeRelationship(0);
outerDiv.remove();
}
buttons.append(buttonc);
const outerDiv=genuserstrip(user,buttons);
container.append(outerDiv);
}
}
}
pending.onclick=genPending;
channelTopic.append(pending);
}
{
const blocked=document.createElement("button");
blocked.textContent=I18n.getTranslation("friends.blocked");
const genBlocked=()=>{
this.localuser.relationshipsUpdate=genBlocked;
checkVoid();
container.innerHTML="";
container.append(I18n.getTranslation("friends.blockedusers"));
for(const user of this.localuser.inrelation){
if(user.relationshipType===2){
const buttonc=document.createElement("div");
const button1=document.createElement("span");
button1.classList.add("svg-x","svgicon");
buttonc.append(button1);
buttonc.classList.add("friendlyButton");
buttonc.onclick=(e)=>{
user.changeRelationship(0);
e.stopImmediatePropagation();
outerDiv.remove();
}
const outerDiv=genuserstrip(user,buttonc);
container.append(outerDiv);
}
}
}
blocked.onclick=genBlocked;
channelTopic.append(blocked);
}
{
const add=document.createElement("button");
add.textContent=I18n.getTranslation("friends.addfriend");
add.onclick=()=>{
this.localuser.relationshipsUpdate=()=>{};
container.innerHTML="";
const float=new Float("");
const options=float.options;
const form=options.addForm("",(e:any)=>{
console.log(e);
if(e.code===404){
throw new FormError(text,I18n.getTranslation("friends.notfound"));
}else if(e.code===400){
throw new FormError(text,e.message.split("Error: ")[1]);
}else{
const box=text.input.deref();
if(!box)return;
box.value="";
}
},{
method:"POST",
fetchURL:this.info.api+"/users/@me/relationships",
headers:this.headers
});
const text=form.addTextInput(I18n.getTranslation("friends.addfriendpromt"),"username");
form.addPreprocessor((obj:any)=>{
const [username,discriminator]=obj.username.split("#");
obj.username=username;
obj.discriminator=discriminator;
if(!discriminator){
throw new FormError(text,I18n.getTranslation("friends.discnotfound"));
}
});
container.append(float.generateHTML());
}
channelTopic.append(add);
}
}
giveMember(_member: memberjson){
console.error("not a real guild, can't give member object");
}
@ -202,6 +415,10 @@ class Group extends Channel{
loading.classList.add("loading");
this.rendertyping();
(document.getElementById("typebox") as HTMLDivElement).contentEditable ="" + true;
(document.getElementById("upload") as HTMLElement).style.visibility="visible";
(document.getElementById("typediv") as HTMLElement).style.visibility="visible";
(document.getElementById("typebox") as HTMLDivElement).focus();
await this.putmessages();
await prom;
this.localuser.getSidePannel();
@ -209,10 +426,7 @@ class Group extends Channel{
return;
}
this.buildmessages();
(document.getElementById("typebox") as HTMLDivElement).contentEditable ="" + true;
(document.getElementById("upload") as HTMLElement).style.visibility="visible";
(document.getElementById("typediv") as HTMLElement).style.visibility="visible";
(document.getElementById("typebox") as HTMLDivElement).focus();
}
messageCreate(messagep: { d: messagejson }){
const messagez = new Message(messagep.d, this);

View file

@ -626,7 +626,7 @@ class Guild extends SnowFlake{
if(addstate){
history.pushState([this.id,undefined], "", "/channels/" + this.id);
}
this.localuser.pageTitle("Weird spot");
this.localuser.pageTitle(I18n.getTranslation("guild.emptytitle"));
const channelTopic = document.getElementById("channelTopic") as HTMLSpanElement;
channelTopic.setAttribute("hidden", "");
@ -640,7 +640,7 @@ class Guild extends SnowFlake{
}
const h1=document.createElement("h1");
h1.classList.add("messagecontainer")
h1.textContent="You're in a weird spot, this guild has no channels";
h1.textContent=I18n.getTranslation("guild.emptytext");
messages.append(h1);
}
loadGuild(){

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><g fill="red"><circle cx="71" cy="31.3" r="27.5"/><path d="M72.2 62.1A48.2 48.2 0 0 0 23.5 110l-.6 70h96.5l.3-32a13.5 13.5 0 0 1-12-13.2v-10.2H97.4A13.5 13.5 0 0 1 84 111.1a13.5 13.5 0 0 1 13.5-13.5h10.1V87.5a13.5 13.5 0 0 1 2.1-7 48.2 48.2 0 0 0-37.5-18.4z" overflow="visible"/><path stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="15" d="M121.1 87.5v47.3m23.7-23.7H97.5"/></g></svg>

After

Width:  |  Height:  |  Size: 472 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"><g fill="red"><circle cx="97.7" cy="97.7" r="82.3"/><path d="M0 180h90v-80Z"/></g></svg>

After

Width:  |  Height:  |  Size: 150 B

View file

@ -59,7 +59,7 @@
<span class="svgicon svg-category"></span>
</label>
<input type="checkbox" id="maintoggle">
<span class="flexltr">
<span class="flexltr" style="align-items: center;">
<span id="channelname">Channel name</span>
<span id="channelTopic" class="ellipsis" hidden>Channel topic</span>
</span>

View file

@ -505,7 +505,25 @@ roleCreate | {
op:9,
d:boolean,
s:number
}|memberlistupdatejson|voiceupdate|voiceserverupdate;
}|memberlistupdatejson|voiceupdate|voiceserverupdate|{
op: 0,
t: "RELATIONSHIP_ADD",
d: {
id: string,
type: 0|1|2|3|4|5|6,
user: userjson
},
s: number
}|{
op: 0,
t: "RELATIONSHIP_REMOVE",
d: {
id: string,
type: number,
nickname: null
},
s: number
};
type memberChunk = {

View file

@ -124,12 +124,14 @@ class Localuser{
const user = new User(thing.user, this);
user.nickname = thing.nickname;
user.relationshipType = thing.type;
this.inrelation.add(user);
}
this.pingEndpoint();
this.userinfo.updateLocal();
}
inrelation=new Set<User>();
outoffocus(): void{
const servers = document.getElementById("servers") as HTMLDivElement;
servers.innerHTML = "";
@ -365,6 +367,7 @@ class Localuser{
});
await promise;
}
relationshipsUpdate=()=>{};
async handleEvent(temp: wsjson){
console.debug(temp);
if(temp.s)this.lastSequence = temp.s;
@ -539,6 +542,23 @@ class Localuser{
guild.memberupdate(temp.d)
break
}
case "RELATIONSHIP_ADD":{
const user = new User(temp.d.user, this);
user.nickname = null;
user.relationshipType = temp.d.type;
this.inrelation.add(user);
this.relationshipsUpdate();
break;
}
case "RELATIONSHIP_REMOVE":{
const user = this.userMap.get(temp.d.id);
if(!user) return;
user.nickname = null;
user.relationshipType = 0;
this.inrelation.delete(user);
this.relationshipsUpdate();
break;
}
default :{
//@ts-ignore
console.warn("Unhandled case "+temp.t,temp);

View file

@ -516,11 +516,27 @@ class HtmlArea implements OptionsElement<void>{
}
watchForChange(){}
}
/**
* This is a simple wrapper class for Options to make it happy so it can be used outside of Settings.
*/
class Float{
options:Options;
/**
* This is a simple wrapper class for Options to make it happy so it can be used outside of Settings.
*/
constructor(name:string, options={ ltr:false, noSubmit:false}){
this.options=new Options(name,this,options)
}
changed=()=>{};
generateHTML(){
return this.options.generateHTML();
}
}
class Options implements OptionsElement<void>{
name: string;
haschanged = false;
readonly options: OptionsElement<any>[];
readonly owner: Buttons | Options | Form;
readonly owner: Buttons | Options | Form | Float;
readonly ltr: boolean;
value!: void;
readonly html: WeakMap<OptionsElement<any>, WeakRef<HTMLDivElement>> = new WeakMap();
@ -530,7 +546,7 @@ class Options implements OptionsElement<void>{
);
constructor(
name: string,
owner: Buttons | Options | Form,
owner: Buttons | Options | Form | Float,
{ ltr = false, noSubmit=false} = {}
){
this.name = name;
@ -1145,38 +1161,84 @@ class Form implements OptionsElement<object>{
}
console.log("middle2");
await Promise.allSettled(promises);
try{
this.preprocessor(build);
}catch(e){
if(e instanceof FormError){
const elm = this.options.html.get(e.elem);
if(elm){
const html = elm.deref();
if(html){
this.makeError(html, e.message);
}
}
}
return;
}
if(this.fetchURL !== ""){
fetch(this.fetchURL, {
method: this.method,
body: JSON.stringify(build),
headers: this.headers,
})
.then(_=>_.json())
.then(_=>{
return _.text()
}).then(_=>{
if(_==="") return {};
return JSON.parse(_)
})
.then(json=>{
if(json.errors && this.errors(json.errors))return;
if(json.errors){
if(this.errors(json)){
return;
}
}
try{
this.onSubmit(json);
}catch(e){
console.error(e);
if(e instanceof FormError){
const elm = this.options.html.get(e.elem);
if(elm){
const html = elm.deref();
if(html){
this.makeError(html, e.message);
}
}
}
return;
}
});
}else{
try{
this.onSubmit(build);
}catch(e){
if(e instanceof FormError){
const elm = this.options.html.get(e.elem);
if(elm){
const html = elm.deref();
if(html){
this.makeError(html, e.message);
}
}
}
return;
}
}
console.warn("needs to be implemented");
}
errors(errors: {
code: number;
message: string;
errors: { [key: string]: { _errors: { message: string; code: string } } };
}){
errors(errors: {code: number; message: string; errors: { [key: string]: { _errors: { message: string; code: string }[] } }}){
if(!(errors instanceof Object)){
return;
}
for(const error of Object.keys(errors)){
for(const error of Object.keys(errors.errors)){
const elm = this.names.get(error);
if(elm){
const ref = this.options.html.get(elm);
if(ref && ref.deref()){
const html = ref.deref() as HTMLDivElement;
this.makeError(html, errors.errors[error]._errors.message);
const errorMessage=errors.errors[error]._errors[0].message;
this.makeError(html, errorMessage);
return true;
}
}
@ -1253,4 +1315,4 @@ class Settings extends Buttons{
}
}
export{ Settings, OptionsElement, Buttons, Options,Form };
export{ Settings, OptionsElement, Buttons, Options,Form,Float };

View file

@ -256,9 +256,15 @@ textarea {
}
.svg-friends{
mask: url(/icons/friends.svg);
width: 24px !important;!i;!;
height: 24px !important;!i;!;
margin-right: 0 !important;!i;!;
width: 24px !important;
height: 24px !important;
margin-right: 0 !important;
}
.svg-frmessage{
mask: url(/icons/frmessage.svg);
}
.svg-addfriend{
mask: url(/icons/addfriend.svg);
}
.svgicon {
display: block;
@ -833,6 +839,9 @@ span.instanceStatus {
margin: auto 0 0 8px;
font-size: .9em;
color: var(--primary-text-soft);
button{
margin-right:.05in;
}
}
#channelTopic[hidden] {
display: none;
@ -2045,7 +2054,18 @@ fieldset input[type="radio"] {
margin: 6px 12px;
}
}
.friendcontainer{
display: flex;
width: 100%;
padding: .2in;
>div{
background:#00000030;
margin-bottom:.1in;
padding:.06in .1in;
border-radius:.1in;
border: solid 1px var(--black);
}
}
.fixedsearch{
position: absolute;
background: var(--primary-bg);
@ -2064,6 +2084,35 @@ fieldset input[type="radio"] {
}
}
.suberror{
animation: goout 6s forwards;
}
.suberrora{
background:var(--channel-hover);
border-radius:.1in;
position:absolute;
border:solid var(--primary-text) .02in;
color:color-mix(in hsl,var(--yellow),var(--red));
font-weight:bold;
opacity:0;
cursor:default;
/* height: .4in; */
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-evenly;
padding: .075in;
box-sizing: border-box;
pointer-events: none;
}
@keyframes goout {
0%,100%{
opacity:0;
}
5%,90%{
opacity:1;
}
}
.friendsbutton{
transition: background-color .2s;
background-color: #00000050;
@ -2072,3 +2121,16 @@ fieldset input[type="radio"] {
.bigemoji{
width:.6in;
}
.friendlyButton{
padding: .07in;
background: #00000045;
transition:background .2s;
border-radius: 1in;
border: solid 1px var(--black);
width: 24px;
height: 24px;
margin: 0 .05in;
}
.friendlyButton:hover{
background:black;
}

View file

@ -8,6 +8,7 @@ import{ presencejson, userjson }from"./jsontypes.js";
import { Role } from "./role.js";
import { Search } from "./search.js";
import { I18n } from "./i18n.js";
import { Direct } from "./direct.js";
class User extends SnowFlake{
owner: Localuser;
@ -15,7 +16,7 @@ class User extends SnowFlake{
avatar!: string | null;
username!: string;
nickname: string | null = null;
relationshipType: 0 | 1 | 2 | 3 | 4 = 0;
relationshipType: 0 | 1 | 2 | 3 | 4 | 5 | 6 = 0;
bio!: MarkDown;
discriminator!: string;
pronouns!: string;
@ -85,23 +86,26 @@ class User extends SnowFlake{
this.setstatus("offline");
}
}
get online(){
return (this.status)&&(this.status!="offline");
}
setstatus(status: string): void{
this.status = status;
}
async getStatus(): Promise<string>{
getStatus(): string{
return this.status || "offline";
}
static contextmenu = new Contextmenu<User, Member | undefined>("User Menu");
static setUpContextMenu(): void{
this.contextmenu.addbutton(()=>I18n.getTranslation("user.copyId"), function(this: User){
navigator.clipboard.writeText(this.id);
});
this.contextmenu.addbutton(()=>I18n.getTranslation("user.message"), function(this: User){
fetch(this.info.api + "/users/@me/channels", {
async opendm(){
for(const dm of (this.localuser.guildids.get("@me") as Direct).channels){
if(dm.user.id===this.id){
this.localuser.goToChannel(dm.id);
return;
}
}
await fetch(this.info.api + "/users/@me/channels", {
method: "POST",
body: JSON.stringify({ recipients: [this.id] }),
headers: this.localuser.headers,
@ -110,6 +114,31 @@ class User extends SnowFlake{
.then(json=>{
this.localuser.goToChannel(json.id);
});
return;
}
async changeRelationship(type:0|1|2|3|4|5){
if(type!==0){
await fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
method: "PUT",
headers: this.owner.headers,
body: JSON.stringify({
type,
}),
});
}else{
await fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
method: "DELETE",
headers: this.owner.headers
});
}
this.relationshipType=type;
}
static setUpContextMenu(): void{
this.contextmenu.addbutton(()=>I18n.getTranslation("user.copyId"), function(this: User){
navigator.clipboard.writeText(this.id);
});
this.contextmenu.addbutton(()=>I18n.getTranslation("user.message"), function(this: User){
this.opendm();
});
this.contextmenu.addbutton(
()=>I18n.getTranslation("user.block"),
@ -133,13 +162,7 @@ class User extends SnowFlake{
}
);
this.contextmenu.addbutton(()=>I18n.getTranslation("user.friendReq"), function(this: User){
fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
method: "PUT",
headers: this.owner.headers,
body: JSON.stringify({
type: 1,
}),
});
this.changeRelationship(1);
});
this.contextmenu.addbutton(
()=>I18n.getTranslation("user.kick"),
@ -370,15 +393,8 @@ class User extends SnowFlake{
);
}
block(): void{
fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
method: "PUT",
headers: this.owner.headers,
body: JSON.stringify({
type: 2,
}),
});
this.relationshipType = 2;
async block(){
await this.changeRelationship(2);
const channel = this.localuser.channelfocus;
if(channel){
for(const message of channel.messages){
@ -387,12 +403,8 @@ class User extends SnowFlake{
}
}
unblock(): void{
fetch(`${this.info.api}/users/@me/relationships/${this.id}`, {
method: "DELETE",
headers: this.owner.headers,
});
this.relationshipType = 0;
async unblock(){
await this.changeRelationship(0);
const channel = this.localuser.channelfocus;
if(channel){
for(const message of channel.messages){

View file

@ -229,7 +229,9 @@
"noDelete":"Nevermind",
"create":"Create guild",
"loadingDiscovery":"Loading...",
"disoveryTitle":"Guild discovery ($1) {{PLURAL:$1|entry|entries}}"
"disoveryTitle":"Guild discovery ($1) {{PLURAL:$1|entry|entries}}",
"emptytitle":"Weird spot",
"emptytext":"You're in a weird spot, this guild has no channels"
},
"role":{
"displaySettings":"Display settings",
@ -345,11 +347,28 @@
"joinUsing":"Join using invite",
"inviteLinkCode":"Invite Link/Code"
},
"friends":{
"blocked":"Blocked",
"blockedusers":"Blocked Users:",
"addfriend":"Add Friend",
"addfriendpromt":"Add friends by username:",
"notfound":"User not found",
"discnotfound":"Discriminator not found",
"pending":"Pending",
"pending:":"Pending friend requests:",
"all":"All",
"all:":"All friends:",
"online":"Online",
"online:":"Online friends:",
"friendlist":"Friend List",
"friends":"Friends"
},
"replyingTo":"Replying to $1",
"DMs":{
"copyId":"Copy DM id",
"markRead":"Mark as read",
"close":"Close DM"
"close":"Close DM",
"name":"Dirrect Messages"
},
"user":{
"copyId":"Copy user ID",