Comments & Updated caching
This commit is contained in:
parent
9983720bd1
commit
2781724f8f
16
src/comp/comment.svelte
Normal file
16
src/comp/comment.svelte
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* @type {import("$types/base").CommentTreeNode}
|
||||||
|
*/
|
||||||
|
export let commentNode;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h5>{commentNode.parent.author?.name}</h5>
|
||||||
|
<p>{commentNode.parent.content}</p>
|
||||||
|
<div>
|
||||||
|
{#each commentNode.children as reply}
|
||||||
|
<svelte:self commentNode={reply}></svelte:self>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
41
src/lib/client/nodetree.js
Normal file
41
src/lib/client/nodetree.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {import('$types/base').Comment} Comment
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('$types/base').Result<Comment>} comments
|
||||||
|
* @returns {import('$types/base').CommentTreeNode[]}
|
||||||
|
*/
|
||||||
|
export function buildCommentTree(comments) {
|
||||||
|
/** @type {Comment[]} */
|
||||||
|
let roots = [];
|
||||||
|
/** @type {Map<number, Comment[]>} */
|
||||||
|
let refs = new Map();
|
||||||
|
|
||||||
|
comments.forEach((comment, id) => {
|
||||||
|
if (comment.parentCommentId == null) {
|
||||||
|
roots.push(comment);
|
||||||
|
} else {
|
||||||
|
if (!refs.has(comment.parentCommentId)) {
|
||||||
|
refs.set(comment.parentCommentId, []);
|
||||||
|
}
|
||||||
|
refs.get(comment.parentCommentId)?.push(comment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return roots.map(r => buildFromRoot(refs, r));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Map<number, Comment[]>} refs
|
||||||
|
* @param {Comment} comment
|
||||||
|
* @returns {import('$types/base').CommentTreeNode}
|
||||||
|
*/
|
||||||
|
function buildFromRoot(refs, comment) {
|
||||||
|
return {
|
||||||
|
parent: comment,
|
||||||
|
children: refs.get(comment.id)?.map(c => {
|
||||||
|
return buildFromRoot(refs, c);
|
||||||
|
}) || []
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { createCache } from '$lib/cache.server';
|
import { createCache } from '$lib/cache.server';
|
||||||
import { cacheUpdater, cachedMethod } from './root';
|
import { cacheUpdater, cachedMethod, refExtendCachedMethod } from './root';
|
||||||
|
|
||||||
const cache = createCache();
|
const cache = createCache();
|
||||||
|
|
||||||
@ -12,6 +12,17 @@ const cache = createCache();
|
|||||||
* @typedef {import('$types/base').Category} Category
|
* @typedef {import('$types/base').Category} Category
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('postgres').Row} row
|
||||||
|
* @returns {Category}
|
||||||
|
*/
|
||||||
|
function parseCategoryFromRow(row) {
|
||||||
|
return {
|
||||||
|
id: row['id'],
|
||||||
|
name: row['name']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Result<Category>} categories
|
* @param {Result<Category>} categories
|
||||||
* @returns {Result<Category>}
|
* @returns {Result<Category>}
|
||||||
@ -25,13 +36,15 @@ const updateCategoryCache = cacheUpdater(cache);
|
|||||||
*/
|
*/
|
||||||
export const getCategoriesCached = cachedMethod(cache, getCategories);
|
export const getCategoriesCached = cachedMethod(cache, getCategories);
|
||||||
|
|
||||||
|
export const getCategoriesCachedByRef = refExtendCachedMethod(getCategoriesCached);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('postgres').Sql} sql
|
* @param {import('postgres').Sql} sql
|
||||||
* @param {number[]} category_ids
|
* @param {number[]} category_ids
|
||||||
* @returns {Promise<Result<Category>>}
|
* @returns {Promise<Result<Category>>}
|
||||||
*/
|
*/
|
||||||
export async function getCategories(sql, category_ids) {
|
export async function getCategories(sql, category_ids) {
|
||||||
if (category_ids.length == 0) return {};
|
if (category_ids.length == 0) return new Map();
|
||||||
|
|
||||||
const query = sql`
|
const query = sql`
|
||||||
SELECT id, name
|
SELECT id, name
|
||||||
@ -43,13 +56,10 @@ export async function getCategories(sql, category_ids) {
|
|||||||
/**
|
/**
|
||||||
* @type {Result<Category>}
|
* @type {Result<Category>}
|
||||||
*/
|
*/
|
||||||
let result = {};
|
let result = new Map();
|
||||||
|
|
||||||
categories.forEach(row => {
|
categories.forEach(row => {
|
||||||
result[row['id']] = {
|
result.set(row['id'], parseCategoryFromRow(row));
|
||||||
id: row['id'],
|
|
||||||
name: row['name']
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return updateCategoryCache(result);
|
return updateCategoryCache(result);
|
||||||
@ -64,12 +74,8 @@ export async function getCategories(sql, category_ids) {
|
|||||||
export async function getCategoryCached(sql, category_id) {
|
export async function getCategoryCached(sql, category_id) {
|
||||||
const categories = await getCategoriesCached(sql, [category_id]);
|
const categories = await getCategoriesCached(sql, [category_id]);
|
||||||
|
|
||||||
if (Object.keys(categories).length == 0) {
|
return categories.get(category_id) || {
|
||||||
return {
|
|
||||||
error: true,
|
error: true,
|
||||||
msg: `Could not find Category of ID ${category_id}`
|
msg: `Could not find Category of ID ${category_id}`
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return categories[category_id];
|
|
||||||
}
|
}
|
||||||
48
src/lib/server/db/comment.js
Normal file
48
src/lib/server/db/comment.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { getUser, getUsersCached, getUsersCachedByRef } from './user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('$types/base').Comment} Comment
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('$types/base').User | null} author
|
||||||
|
* @param {import('postgres').Row} row
|
||||||
|
* @returns {Comment}
|
||||||
|
*/
|
||||||
|
function parseCommentFromRow(author, row) {
|
||||||
|
return {
|
||||||
|
id: row['id'],
|
||||||
|
author: author,
|
||||||
|
parentCommentId: row['parent_comment_id'],
|
||||||
|
content: row['latest_content'],
|
||||||
|
commentDate: row['created_date'],
|
||||||
|
rating: {
|
||||||
|
likes: BigInt(row['likes']),
|
||||||
|
dislikes: BigInt(row['dislikes']),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('postgres').Sql} sql
|
||||||
|
* @param {number} post_id
|
||||||
|
* @returns {Promise<Comment[]>}
|
||||||
|
*/
|
||||||
|
export async function getCommentsForPost(sql, post_id) {
|
||||||
|
const query = sql`
|
||||||
|
SELECT id, author_id, latest_content, parent_comment_id, created_date, likes, dislikes
|
||||||
|
FROM doki8902.message_comment
|
||||||
|
WHERE post_id = ${ post_id };`;
|
||||||
|
|
||||||
|
const comments = await query;
|
||||||
|
|
||||||
|
const users = await getUsersCachedByRef(sql, comments, c => c['author_id']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Comment[]}
|
||||||
|
*/
|
||||||
|
return comments.map(row => parseCommentFromRow(
|
||||||
|
users.get(row['author_id']) || null,
|
||||||
|
row
|
||||||
|
));
|
||||||
|
}
|
||||||
@ -1,10 +1,31 @@
|
|||||||
import { getCategoriesCached, getCategoryCached } from './category';
|
import { getCategoriesCached, getCategoriesCachedByRef, getCategoryCached } from './category';
|
||||||
import { getUser, getUsersCached } from './user';
|
import { getUser, getUsersCached, getUsersCachedByRef } from './user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('$types/base').Post} Post
|
* @typedef {import('$types/base').Post} Post
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('$types/base').User | null} author
|
||||||
|
* @param {import('$types/base').Category} category
|
||||||
|
* @param {import('postgres').Row} row
|
||||||
|
* @returns {Post}
|
||||||
|
*/
|
||||||
|
function parsePostFromRow(author, category, row) {
|
||||||
|
return {
|
||||||
|
id: row['id'],
|
||||||
|
author: author,
|
||||||
|
name: row['name'],
|
||||||
|
category: category,
|
||||||
|
content: row['latest_content'],
|
||||||
|
postDate: row['created_date'],
|
||||||
|
rating: {
|
||||||
|
likes: BigInt(row['likes']),
|
||||||
|
dislikes: BigInt(row['dislikes']),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('postgres').Sql} sql
|
* @param {import('postgres').Sql} sql
|
||||||
* @param {import('$types/base').Category | undefined} category
|
* @param {import('$types/base').Category | undefined} category
|
||||||
@ -30,31 +51,17 @@ export async function getPosts(sql, category = undefined, limit = 10, offset = 0
|
|||||||
|
|
||||||
const posts = await query;
|
const posts = await query;
|
||||||
|
|
||||||
const users = await getUsersCached(sql, posts.map(row => {
|
const users = await getUsersCachedByRef(sql, posts, p => p['author_id']);
|
||||||
return row['author_id'];
|
const categories = await getCategoriesCachedByRef(sql, posts, p => p['category_id']);
|
||||||
}));
|
|
||||||
|
|
||||||
const categories = await getCategoriesCached(sql, posts.map(row => {
|
|
||||||
return row['category_id'];
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Post[]}
|
* @type {Post[]}
|
||||||
*/
|
*/
|
||||||
return posts.map(row => {
|
return posts.map(row => parsePostFromRow(
|
||||||
return {
|
users.get(row['author_id']) || null,
|
||||||
id: row['id'],
|
/** @type {import('$types/base').Category} */ (categories.get(row['category_id'])),
|
||||||
author: users[row['author_id']] || null,
|
row
|
||||||
name: row['name'],
|
));
|
||||||
category: categories[row['category_id']],
|
|
||||||
content: row['latest_content'],
|
|
||||||
post_date: row['created_date'],
|
|
||||||
rating: {
|
|
||||||
likes: BigInt(row['likes']),
|
|
||||||
dislikes: BigInt(row['dislikes']),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,16 +115,5 @@ export async function getPost(sql, post_id) {
|
|||||||
/**
|
/**
|
||||||
* @type {Post}
|
* @type {Post}
|
||||||
*/
|
*/
|
||||||
return {
|
return parsePostFromRow(author, category, post);
|
||||||
id: post['id'],
|
|
||||||
author: author,
|
|
||||||
name: post['name'],
|
|
||||||
category: category,
|
|
||||||
content: post['latest_content'],
|
|
||||||
post_date: post['created_date'],
|
|
||||||
rating: {
|
|
||||||
likes: BigInt(post['likes']),
|
|
||||||
dislikes: BigInt(post['dislikes']),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @param {import('node-cache')} cache
|
* @param {import('node-cache')} cache
|
||||||
* @returns {function({[id: number]: T})}
|
* @returns {function(import('$types/base').Result<T>)}
|
||||||
*/
|
*/
|
||||||
export const cacheUpdater = (cache) => {
|
export const cacheUpdater = (cache) => {
|
||||||
return function updateUserCache(data) {
|
return function updateUserCache(data) {
|
||||||
Object.keys(data).forEach(id => {
|
data.forEach((val, id) => {
|
||||||
cache.set(parseInt(id), data[parseInt(id)]);
|
cache.set(id, val);
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@ -15,15 +15,15 @@ export const cacheUpdater = (cache) => {
|
|||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @param {import('node-cache')} cache
|
* @param {import('node-cache')} cache
|
||||||
* @param {function(import('postgres').Sql, number[]): Promise<{[id: number]: T}>} method
|
* @param {function(import('postgres').Sql, number[]): Promise<import('$types/base').Result<T>>} method
|
||||||
* @returns {function(import('postgres').Sql, number[]): Promise<{[id: number]: T}>}
|
* @returns {function(import('postgres').Sql, number[]): Promise<import('$types/base').Result<T>>}
|
||||||
*/
|
*/
|
||||||
export const cachedMethod = (cache, method) => {
|
export const cachedMethod = (cache, method) => {
|
||||||
return async function(sql, ids) {
|
return async function(sql, ids) {
|
||||||
/**
|
/**
|
||||||
* @type {{[id: number]: T}}
|
* @type {import('$types/base').Result<T>}
|
||||||
*/
|
*/
|
||||||
let results = {};
|
let results = new Map();
|
||||||
/**
|
/**
|
||||||
* @type {number[]}
|
* @type {number[]}
|
||||||
*/
|
*/
|
||||||
@ -34,13 +34,24 @@ export const cachedMethod = (cache, method) => {
|
|||||||
return;
|
return;
|
||||||
let user = cache.get(id);
|
let user = cache.get(id);
|
||||||
if (user)
|
if (user)
|
||||||
results[id] = user;
|
results.set(id, user);
|
||||||
else
|
else
|
||||||
missing.push(id);
|
missing.push(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
const remaining = await method(sql, missing);
|
const remaining = await method(sql, missing);
|
||||||
|
|
||||||
return Object.assign({}, results, remaining);
|
return new Map([...results, ...remaining]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {function(import('postgres').Sql, number[]): Promise<import('$types/base').Result<T>>} cachedMethod
|
||||||
|
* @returns {function(import('postgres').Sql, import('postgres').RowList<import('postgres').Row[]>, function(import('postgres').Row): number): Promise<import('$types/base').Result<T>>}
|
||||||
|
*/
|
||||||
|
export const refExtendCachedMethod = (cachedMethod) => {
|
||||||
|
return async function(sql, rows, getRef) {
|
||||||
|
return await cachedMethod(sql, rows.map(r => getRef(r)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { createCache } from '$lib/cache.server';
|
import { createCache } from '$lib/cache.server';
|
||||||
import { cacheUpdater, cachedMethod } from './root';
|
import { cacheUpdater, cachedMethod, refExtendCachedMethod } from './root';
|
||||||
|
|
||||||
const cache = createCache();
|
const cache = createCache();
|
||||||
|
|
||||||
@ -12,6 +12,18 @@ const cache = createCache();
|
|||||||
* @typedef {import('$types/base').User} User
|
* @typedef {import('$types/base').User} User
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('postgres').Row} row
|
||||||
|
* @returns {User}
|
||||||
|
*/
|
||||||
|
function parseUserFromRow(row) {
|
||||||
|
return {
|
||||||
|
id: row['id'],
|
||||||
|
name: row['username'],
|
||||||
|
joinDate: row['join_time']
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Result<User>} users
|
* @param {Result<User>} users
|
||||||
* @returns {Result<User>}
|
* @returns {Result<User>}
|
||||||
@ -25,13 +37,15 @@ const updateUserCache = cacheUpdater(cache);
|
|||||||
*/
|
*/
|
||||||
export const getUsersCached = cachedMethod(cache, getUsers);
|
export const getUsersCached = cachedMethod(cache, getUsers);
|
||||||
|
|
||||||
|
export const getUsersCachedByRef = refExtendCachedMethod(getUsersCached);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('postgres').Sql} sql
|
* @param {import('postgres').Sql} sql
|
||||||
* @param {number[]} user_ids
|
* @param {number[]} user_ids
|
||||||
* @returns {Promise<Result<User>>}
|
* @returns {Promise<Result<User>>}
|
||||||
*/
|
*/
|
||||||
export async function getUsers(sql, user_ids) {
|
export async function getUsers(sql, user_ids) {
|
||||||
if (user_ids.length == 0) return {};
|
if (user_ids.length == 0) return new Map();
|
||||||
|
|
||||||
const query = sql`
|
const query = sql`
|
||||||
SELECT id, username, join_time
|
SELECT id, username, join_time
|
||||||
@ -43,14 +57,10 @@ export async function getUsers(sql, user_ids) {
|
|||||||
/**
|
/**
|
||||||
* @type {Result<User>}
|
* @type {Result<User>}
|
||||||
*/
|
*/
|
||||||
let result = {};
|
let result = new Map();
|
||||||
|
|
||||||
users.forEach(row => {
|
users.forEach(row => {
|
||||||
result[row['id']] = {
|
result.set(row['id'], parseUserFromRow(row));
|
||||||
id: row['id'],
|
|
||||||
name: row['username'],
|
|
||||||
join_date: row['join_time']
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return updateUserCache(result);
|
return updateUserCache(result);
|
||||||
@ -64,12 +74,8 @@ export async function getUsers(sql, user_ids) {
|
|||||||
export async function getUser(sql, user_id) {
|
export async function getUser(sql, user_id) {
|
||||||
const users = await getUsers(sql, [user_id]);
|
const users = await getUsers(sql, [user_id]);
|
||||||
|
|
||||||
if (Object.keys(users).length == 0) {
|
return users.get(user_id) || {
|
||||||
return {
|
|
||||||
error: true,
|
error: true,
|
||||||
msg: `Could not find user of ID ${user_id}`
|
msg: `Could not find user of ID ${user_id}`
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return users[user_id];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,6 @@ import { getPosts } from '$lib/server/db/post';
|
|||||||
export async function load({ locals }) {
|
export async function load({ locals }) {
|
||||||
let result = await getPosts(locals.sql);
|
let result = await getPosts(locals.sql);
|
||||||
|
|
||||||
console.log(result);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
posts: result
|
posts: result
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,18 @@
|
|||||||
|
import { getCommentsForPost } from "$lib/server/db/comment";
|
||||||
import { getPost } from "$lib/server/db/post";
|
import { getPost } from "$lib/server/db/post";
|
||||||
|
|
||||||
/** @type {import("@sveltejs/kit").ServerLoad} */
|
/** @type {import("@sveltejs/kit").ServerLoad} */
|
||||||
export async function load({ params, locals }) {
|
export async function load({ params, locals }) {
|
||||||
/** @type {import("$types/base").Post | import("$types/error").Error} */
|
const post_id = Number(params.id);
|
||||||
const post = await getPost(locals.sql, Number(params.id));
|
|
||||||
|
const post = await getPost(locals.sql, post_id);
|
||||||
|
|
||||||
|
const comments = await getCommentsForPost(locals.sql, post_id);
|
||||||
|
|
||||||
|
console.log(comments);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
post: post
|
post: post,
|
||||||
|
comments: comments
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,29 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import Comment from "$comp/comment.svelte";
|
||||||
|
import { buildCommentTree } from "$lib/client/nodetree";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {{post: import("$types/base").Post}}
|
* @type {{
|
||||||
|
* post: import("$types/base").Post,
|
||||||
|
* comments: import("$types/base").Result<import("$types/base").Comment>
|
||||||
|
* }}
|
||||||
*/
|
*/
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('$types/base').CommentTreeNode[]}
|
||||||
|
*/
|
||||||
|
let commentTree;
|
||||||
|
$: commentTree = buildCommentTree(data.comments);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>{data.post.name}</h1>
|
<h1>{data.post.name}</h1>
|
||||||
<a href="#">{data.post.author?.name}</a>
|
<a href="#">{data.post.author?.name}</a>
|
||||||
<p>{data.post.content}</p>
|
<p>{data.post.content}</p>
|
||||||
|
<div>
|
||||||
|
{#each commentTree as reply}
|
||||||
|
<Comment commentNode={reply}></Comment>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
export type Result<T> = {[id: number]: T};
|
export type Result<T> = Map<number, T>;
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
join_date: Date
|
joinDate: Date
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Rating = {
|
export type Rating = {
|
||||||
@ -22,6 +22,20 @@ export type Post = {
|
|||||||
name: string,
|
name: string,
|
||||||
category: Category,
|
category: Category,
|
||||||
content: string,
|
content: string,
|
||||||
post_date: Date,
|
postDate: Date,
|
||||||
rating: Rating
|
rating: Rating
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Comment = {
|
||||||
|
id: number,
|
||||||
|
author: User | null,
|
||||||
|
content: string,
|
||||||
|
commentDate: Date,
|
||||||
|
rating: Rating,
|
||||||
|
parentCommentId: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CommentTreeNode = {
|
||||||
|
parent: Comment,
|
||||||
|
children: (CommentTreeNode | number)[]
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user