Fixed long-polling and made some improvements in messages
This commit is contained in:
parent
e12c0fee36
commit
01a1ce097c
12 changed files with 133 additions and 51 deletions
|
@ -8,17 +8,7 @@ $(document).ready(function () {
|
||||||
var nodes = cmsgs.children;
|
var nodes = cmsgs.children;
|
||||||
for (var x = 0; x < nodes.length; x++) {
|
for (var x = 0; x < nodes.length; x++) {
|
||||||
var item = nodes[x];
|
var item = nodes[x];
|
||||||
console.log(item);
|
handlereceivedmessage(item);
|
||||||
var spans = item.getElementsByTagName("span");
|
|
||||||
var ctime = null;
|
|
||||||
for (var i = 0; i < spans.length; i++)
|
|
||||||
if (spans[i].className.split(' ').indexOf("converttime") > -1)
|
|
||||||
ctime = spans[i];
|
|
||||||
if (ctime != null)
|
|
||||||
console.log(ctime.innerText);
|
|
||||||
if (ctime != null)
|
|
||||||
ctime.innerText = moment(ctime.innerText, "YYYY-MM-DDTHH:mm:ssZ").fromNow(); //.format("lll");
|
|
||||||
}
|
}
|
||||||
cmsgs.lastElementChild.scrollIntoView(false);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,30 @@
|
||||||
(function poll() {
|
var handlereceivedmessage = function handlereceivedmessage(msgnode) {
|
||||||
setTimeout(function() {
|
var spans = msgnode.getElementsByTagName("span");
|
||||||
$.ajax({ url: "/receivemessage", success: function(data) {
|
var ctime = null;
|
||||||
|
for (var i = 0; i < spans.length; i++)
|
||||||
|
if (spans[i].className.split(' ').indexOf("converttime") > -1)
|
||||||
|
ctime = spans[i];
|
||||||
|
if (ctime != null)
|
||||||
|
ctime.innerText = moment(ctime.innerText, "YYYY-MM-DDTHH:mm:ssZ").fromNow();
|
||||||
|
msgnode.scrollIntoView(false);
|
||||||
|
}
|
||||||
|
|
||||||
}, dataType: "json", complete: poll });
|
(function poll() {
|
||||||
|
setTimeout(function () {
|
||||||
|
$.ajax({
|
||||||
|
url: "/receivemessage", success: function (data) {
|
||||||
|
console.log(data);
|
||||||
|
var msgelement = document.getElementById("channelmessages").appendChild(document.createElement("div"));
|
||||||
|
var header = msgelement.appendChild(document.createElement("p");
|
||||||
|
header.innerText = data.sender.name + " - ";
|
||||||
|
var isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
|
||||||
|
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
header.appendElement("span").addClass("converttime")
|
||||||
|
.value = isoFormat.format(data.time) + "+00:00";
|
||||||
|
var body = msgelement.appendChild(document.createElement("p"));
|
||||||
|
body.innerText = data.message;
|
||||||
|
handlereceivedmessage(msgnode);
|
||||||
|
}, dataType: "json", complete: poll
|
||||||
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
})();
|
})();
|
|
@ -7,8 +7,8 @@ var sendmsg = function sendmsg(msginputta) {
|
||||||
};
|
};
|
||||||
|
|
||||||
var respfunc = function respfunc(result) {
|
var respfunc = function respfunc(result) {
|
||||||
if (result != "Success") { //on success result is string
|
|
||||||
var msginput = document.getElementById("msginput");
|
var msginput = document.getElementById("msginput");
|
||||||
|
if (result != "Success") { //on success result is string
|
||||||
if (result.responseText.indexOf("JSONERROR") != -1) {
|
if (result.responseText.indexOf("JSONERROR") != -1) {
|
||||||
console.log("Got JSON error. Retrying...");
|
console.log("Got JSON error. Retrying...");
|
||||||
console.log(result.responseText);
|
console.log(result.responseText);
|
||||||
|
@ -19,8 +19,10 @@ var respfunc = function respfunc(result) {
|
||||||
msginput.disabled = false;
|
msginput.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
location.reload(true); //TODO: Don't referesh on message send
|
msginput.value = "";
|
||||||
|
msginput.disabled = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var sendmsgonenter = function sendmsgonenter(e) {
|
var sendmsgonenter = function sendmsgonenter(e) {
|
||||||
|
|
|
@ -14,7 +14,7 @@ function getFormData($form) {
|
||||||
|
|
||||||
function showError(message) {
|
function showError(message) {
|
||||||
var errormsg = document.getElementById("errormsg");
|
var errormsg = document.getElementById("errormsg");
|
||||||
errormsg.innerHTML = result.responseText;
|
errormsg.innerHTML = message;
|
||||||
errormsg.style = "display: block";
|
errormsg.style = "display: block";
|
||||||
setTimeout(function(){errormsg.style.display="none";}, 2000);
|
setTimeout(function () { errormsg.style.display = "none"; }, 2000);
|
||||||
}
|
}
|
|
@ -30,7 +30,8 @@ public class Main {
|
||||||
// https://docs.oracle.com/javase/8/docs/api/
|
// https://docs.oracle.com/javase/8/docs/api/
|
||||||
LogManager.getLogger().log(Level.INFO, "Loading files...");
|
LogManager.getLogger().log(Level.INFO, "Loading files...");
|
||||||
DataManager.init();
|
DataManager.init();
|
||||||
final GsonBuilder gsonBuilder = new GsonBuilder();
|
final GsonBuilder saveGsonBuilder = new GsonBuilder();
|
||||||
|
final GsonBuilder exchangeGsonBuilder = new GsonBuilder();
|
||||||
Reflections rf = new Reflections(new ConfigurationBuilder()
|
Reflections rf = new Reflections(new ConfigurationBuilder()
|
||||||
.setUrls(ClasspathHelper.forClassLoader(ManagedData.class.getClassLoader()))
|
.setUrls(ClasspathHelper.forClassLoader(ManagedData.class.getClassLoader()))
|
||||||
.addClassLoader(ManagedData.class.getClassLoader()).addScanners(new SubTypesScanner())
|
.addClassLoader(ManagedData.class.getClassLoader()).addScanners(new SubTypesScanner())
|
||||||
|
@ -39,11 +40,11 @@ public class Main {
|
||||||
for (Class<? extends ManagedData> data : datas) {
|
for (Class<? extends ManagedData> data : datas) {
|
||||||
if (Modifier.isAbstract(data.getModifiers()))
|
if (Modifier.isAbstract(data.getModifiers()))
|
||||||
continue;
|
continue;
|
||||||
gsonBuilder.registerTypeAdapter(new DataType(LoaderCollection.class, data),
|
saveGsonBuilder.registerTypeAdapter(new DataType(LoaderCollection.class, data),
|
||||||
new LoaderCollectionSerializer());
|
new LoaderCollectionSerializer());
|
||||||
gsonBuilder.registerTypeAdapter(new DataType(LoaderRef.class, data), new LoaderRefSerializer());
|
saveGsonBuilder.registerTypeAdapter(new DataType(LoaderRef.class, data), new LoaderRefSerializer());
|
||||||
}
|
}
|
||||||
gson = gsonBuilder.create();
|
gson = saveGsonBuilder.create();
|
||||||
/*
|
/*
|
||||||
* User user = new User(); user.setName("asd"); user.setEmail("test@test.com"); User user2 = new User(); user2.setName("Teszt"); user2.setEmail("test2@test.com"); // user =
|
* User user = new User(); user.setName("asd"); user.setEmail("test@test.com"); User user2 = new User(); user2.setName("Teszt"); user2.setEmail("test2@test.com"); // user =
|
||||||
* provider.save(user); // user2 = provider.save(user2); user.getContacts().add(user2); user2.getContacts().add(user); LogManager.getLogger().log(Level.DEBUG, "1st's contact: " +
|
* provider.save(user); // user2 = provider.save(user2); user.getContacts().add(user2); user2.getContacts().add(user); LogManager.getLogger().log(Level.DEBUG, "1st's contact: " +
|
||||||
|
|
|
@ -18,7 +18,6 @@ import io.github.norbipeti.chat.server.db.domain.SavedData;
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @author Norbi
|
* @author Norbi
|
||||||
*
|
|
||||||
* @param <T>
|
* @param <T>
|
||||||
*/
|
*/
|
||||||
public class LoaderCollection<T extends SavedData> extends Loader implements List<T> {
|
public class LoaderCollection<T extends SavedData> extends Loader implements List<T> {
|
||||||
|
@ -41,7 +40,7 @@ public class LoaderCollection<T extends SavedData> extends Loader implements Lis
|
||||||
|
|
||||||
public LoaderCollection(LoaderCollection<T> parentofsub, int fromIndex, int toIndex) {
|
public LoaderCollection(LoaderCollection<T> parentofsub, int fromIndex, int toIndex) {
|
||||||
this.cl = parentofsub.cl;
|
this.cl = parentofsub.cl;
|
||||||
idlist = parentofsub.idlist.subList(fromIndex, toIndex);
|
idlist = parentofsub.idlist.subList(fromIndex, toIndex); // TODO: Test
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoaderCollection(Class<T> cl, int capacity) {
|
public LoaderCollection(Class<T> cl, int capacity) {
|
||||||
|
@ -132,8 +131,7 @@ public class LoaderCollection<T extends SavedData> extends Loader implements Lis
|
||||||
/**
|
/**
|
||||||
* Remove an object from this collection
|
* Remove an object from this collection
|
||||||
*
|
*
|
||||||
* @param o
|
* @param o Either the object of type T or the ID
|
||||||
* Either the object of type T or the ID
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean remove(Object o) {
|
public boolean remove(Object o) {
|
||||||
|
@ -228,4 +226,19 @@ public class LoaderCollection<T extends SavedData> extends Loader implements Lis
|
||||||
sb.append("]");
|
sb.append("]");
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Object clone() {
|
||||||
|
Object cloned = null;
|
||||||
|
if (idlist instanceof ArrayList<?>)
|
||||||
|
cloned = ((ArrayList<Long>) idlist).clone();
|
||||||
|
if (cloned == null)
|
||||||
|
return cloned;
|
||||||
|
else {
|
||||||
|
LoaderCollection<T> lc = new LoaderCollection<T>(cl, this.size());
|
||||||
|
if (cloned instanceof List<?>)
|
||||||
|
lc.idlist = (List<Long>) cloned;
|
||||||
|
return lc;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package io.github.norbipeti.chat.server.data;
|
package io.github.norbipeti.chat.server.data;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
@ -14,9 +15,7 @@ import io.github.norbipeti.chat.server.db.domain.ManagedData;
|
||||||
|
|
||||||
// @SuppressWarnings("rawtypes")
|
// @SuppressWarnings("rawtypes")
|
||||||
public class LoaderCollectionSerializer extends TypeAdapter<LoaderCollection<?>> {
|
public class LoaderCollectionSerializer extends TypeAdapter<LoaderCollection<?>> {
|
||||||
// private static final Type returnType = getReturnType();
|
|
||||||
|
|
||||||
// http://stackoverflow.com/a/17300227
|
|
||||||
@Override
|
@Override
|
||||||
public void write(JsonWriter out, LoaderCollection<?> value) throws IOException {
|
public void write(JsonWriter out, LoaderCollection<?> value) throws IOException {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
|
@ -40,9 +39,13 @@ public class LoaderCollectionSerializer extends TypeAdapter<LoaderCollection<?>>
|
||||||
}
|
}
|
||||||
in.beginObject();
|
in.beginObject();
|
||||||
in.nextName();
|
in.nextName();
|
||||||
List<Long> list = new Gson().fromJson(in, new TypeToken<List<Long>>() {
|
List<Long> list;
|
||||||
|
LoaderCollection<? extends ManagedData> itemcol;
|
||||||
|
list = new Gson().fromJson(in, new TypeToken<List<Long>>() {
|
||||||
}.getType());
|
}.getType());
|
||||||
if (!in.nextName().equals("class")) {
|
if (!in.nextName().equals("class"))
|
||||||
|
|
||||||
|
{
|
||||||
new Exception("Error: Next isn't \"class\"").printStackTrace();
|
new Exception("Error: Next isn't \"class\"").printStackTrace();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
package io.github.norbipeti.chat.server.db.domain;
|
package io.github.norbipeti.chat.server.db.domain;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
import javax.persistence.*;
|
import javax.persistence.*;
|
||||||
|
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
|
||||||
|
import io.github.norbipeti.chat.server.Main;
|
||||||
import io.github.norbipeti.chat.server.data.LoaderRef;
|
import io.github.norbipeti.chat.server.data.LoaderRef;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@ -92,4 +100,28 @@ public class Message extends ManagedData {
|
||||||
@Override
|
@Override
|
||||||
protected void init() {
|
protected void init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Element getAsHTML(Element channelmessages) {
|
||||||
|
Element msgelement = channelmessages.appendElement("div");
|
||||||
|
Element header = msgelement.appendElement("p");
|
||||||
|
header.text(getSender().get().getName() + " - ");
|
||||||
|
header.appendElement("span").addClass("converttime").text(formatDate());
|
||||||
|
Element body = msgelement.appendElement("p");
|
||||||
|
body.text(getMessage());
|
||||||
|
return msgelement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObject getAsJson() {
|
||||||
|
JsonObject msgobj = new JsonObject();
|
||||||
|
msgobj.add("sender", Main.gson.toJsonTree(getSender().get()));
|
||||||
|
msgobj.add("message", new JsonPrimitive(getMessage()));
|
||||||
|
msgobj.add("time", new JsonPrimitive(formatDate()));
|
||||||
|
return msgobj;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDate() {
|
||||||
|
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
|
||||||
|
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
return isoFormat.format(getTime()) + "+00:00";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,10 +95,6 @@ public class User extends SavedData {
|
||||||
private User() {
|
private User() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LoaderCollection<User> getUsers() {
|
|
||||||
return DataManager.getAll(User.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getId() {
|
public long getId() {
|
||||||
return id;
|
return id;
|
||||||
|
|
|
@ -2,6 +2,7 @@ package io.github.norbipeti.chat.server.page;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
import org.apache.logging.log4j.Level;
|
import org.apache.logging.log4j.Level;
|
||||||
|
@ -49,6 +50,7 @@ public class IndexPage extends Page {
|
||||||
convs.add(c); // TODO: Handle no conversation open
|
convs.add(c); // TODO: Handle no conversation open
|
||||||
}
|
}
|
||||||
user.getConversations().add(convs.get(0));
|
user.getConversations().add(convs.get(0));
|
||||||
|
convs.get(0).getUsers().add(user);
|
||||||
}
|
}
|
||||||
Conversation conv = user.getConversations().get(0);
|
Conversation conv = user.getConversations().get(0);
|
||||||
Element cide = channelmessages.appendElement("p");
|
Element cide = channelmessages.appendElement("p");
|
||||||
|
@ -56,17 +58,13 @@ public class IndexPage extends Page {
|
||||||
cide.attr("id", "convidp");
|
cide.attr("id", "convidp");
|
||||||
cide.text(Long.toString(conv.getId()));
|
cide.text(Long.toString(conv.getId()));
|
||||||
LogManager.getLogger().log(Level.DEBUG, "Messages: " + conv.getMesssageChunks().size());
|
LogManager.getLogger().log(Level.DEBUG, "Messages: " + conv.getMesssageChunks().size());
|
||||||
for (MessageChunk chunk : conv.getMesssageChunks()) { // TODO: Reverse
|
@SuppressWarnings("unchecked")
|
||||||
|
LoaderCollection<MessageChunk> chunks = (LoaderCollection<MessageChunk>) conv.getMesssageChunks()
|
||||||
|
.clone();
|
||||||
|
Collections.reverse(chunks);
|
||||||
|
for (MessageChunk chunk : chunks) {
|
||||||
for (Message message : chunk.getMessages()) {
|
for (Message message : chunk.getMessages()) {
|
||||||
Element msgelement = channelmessages.appendElement("div");
|
message.getAsHTML(channelmessages);
|
||||||
Element header = msgelement.appendElement("p");
|
|
||||||
header.text(message.getSender().get().getName() + " - ");
|
|
||||||
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
|
|
||||||
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
||||||
header.appendElement("span").addClass("converttime")
|
|
||||||
.text(isoFormat.format(message.getTime()) + "+00:00");
|
|
||||||
Element body = msgelement.appendElement("p");
|
|
||||||
body.text(message.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return doc;
|
return doc;
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
package io.github.norbipeti.chat.server.page;
|
package io.github.norbipeti.chat.server.page;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
|
|
||||||
|
import io.github.norbipeti.chat.server.Main;
|
||||||
|
import io.github.norbipeti.chat.server.data.LoaderRef;
|
||||||
|
import io.github.norbipeti.chat.server.db.domain.Conversation;
|
||||||
|
import io.github.norbipeti.chat.server.db.domain.Message;
|
||||||
import io.github.norbipeti.chat.server.db.domain.User;
|
import io.github.norbipeti.chat.server.db.domain.User;
|
||||||
import io.github.norbipeti.chat.server.io.IOHelper;
|
import io.github.norbipeti.chat.server.io.IOHelper;
|
||||||
|
|
||||||
|
@ -15,6 +24,8 @@ public class ReceiveMessageAjaxPage extends Page {
|
||||||
return "receivemessage";
|
return "receivemessage";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static HashMap<User, HttpExchange> exmap = new HashMap<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handlePage(HttpExchange exchange) throws IOException {
|
public void handlePage(HttpExchange exchange) throws IOException {
|
||||||
User user = IOHelper.GetLoggedInUser(exchange);
|
User user = IOHelper.GetLoggedInUser(exchange);
|
||||||
|
@ -22,7 +33,7 @@ public class ReceiveMessageAjaxPage extends Page {
|
||||||
IOHelper.SendResponse(403, "<p>Please log in to receive messages</p>", exchange);
|
IOHelper.SendResponse(403, "<p>Please log in to receive messages</p>", exchange);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
JsonObject obj = new JsonObject(); // TODO
|
exmap.put(user, exchange);
|
||||||
/*
|
/*
|
||||||
* String message = obj.get("message").getAsString().trim(); int conversation = obj.get("conversation").getAsInt(); if (message.trim().length() == 0) { IOHelper.SendResponse(400,
|
* String message = obj.get("message").getAsString().trim(); int conversation = obj.get("conversation").getAsInt(); if (message.trim().length() == 0) { IOHelper.SendResponse(400,
|
||||||
* "<h1>400 Bad request</h1><p>The message cannot be empty.</p>", exchange); return; } LoaderCollection<Conversation> convos = user.getConversations(); Conversation conv = null;
|
* "<h1>400 Bad request</h1><p>The message cannot be empty.</p>", exchange); return; } LoaderCollection<Conversation> convos = user.getConversations(); Conversation conv = null;
|
||||||
|
@ -33,7 +44,18 @@ public class ReceiveMessageAjaxPage extends Page {
|
||||||
* "Added conversation's message count: " + conv.getMesssageChunks().size());
|
* "Added conversation's message count: " + conv.getMesssageChunks().size());
|
||||||
*/
|
*/
|
||||||
|
|
||||||
IOHelper.SendResponse(200, "Success", exchange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void sendMessageBack(Message msg, Conversation conv) throws IOException {
|
||||||
|
for (User user : conv.getUsers()) {
|
||||||
|
LogManager.getLogger().debug("User: " + user);
|
||||||
|
if (exmap.containsKey(user)) {
|
||||||
|
LogManager.getLogger().debug("Exmap contains user");
|
||||||
|
JsonObject msgobj = msg.getAsJson();
|
||||||
|
IOHelper.SendResponse(200, msgobj.toString(), exmap.get(user));
|
||||||
|
exmap.remove(user);
|
||||||
|
} else
|
||||||
|
LogManager.getLogger().warn("User is not listening: " + user);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,8 @@ public class SendMessageAjaxPage extends Page {
|
||||||
LogManager.getLogger().log(Level.DEBUG,
|
LogManager.getLogger().log(Level.DEBUG,
|
||||||
"Added conversation's message count: " + conv.getMesssageChunks().size());
|
"Added conversation's message count: " + conv.getMesssageChunks().size());
|
||||||
|
|
||||||
|
ReceiveMessageAjaxPage.sendMessageBack(msg, conv);
|
||||||
|
|
||||||
IOHelper.SendResponse(200, "Success", exchange);
|
IOHelper.SendResponse(200, "Success", exchange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue