messages + post formatting

This commit is contained in:
tdgmdev 2023-03-11 12:51:19 -05:00
parent 4c75756abc
commit 34d7d516b5
12 changed files with 359 additions and 38 deletions

View file

@ -40,6 +40,15 @@
<a href='/'> <a href='/'>
<img src='/icon_sanifae.svg' alt='Sanifae Logo'> <img src='/icon_sanifae.svg' alt='Sanifae Logo'>
</a> </a>
{#if data.read > 0}
<a href='/messages'>
<img src='/unread.svg' alt='Messages'>
</a>
{:else}
<a href='/messages'>
<img src='/read.svg' alt='Messages'>
</a>
{/if}
{#if data.username && data.username != 'false'} {#if data.username && data.username != 'false'}
<a href='/users/{data.username}'> <a href='/users/{data.username}'>
{data.username} {data.username}

View file

@ -11,15 +11,24 @@
p { p {
white-space: pre-wrap; white-space: pre-wrap;
} }
.bold, .bolditalic {
font-weight: bold;
}
.italic, .bolditalic {
font-style: italic;
}
</style> </style>
<script> <script>
import {formatPost} from '$lib/util.js'; import {formatPost} from '$lib/util.js';
export let content = ''; export let content = '';
export let excludeImg = false;
let contentSplit; let contentSplit;
$: contentSplit = formatPost(content || ''); $: contentSplit = formatPost(content || '', excludeImg);
console.log(contentSplit); console.log(contentSplit);
</script> </script>
@ -28,6 +37,7 @@
{#each contentSplit as line} {#each contentSplit as line}
<p> <p>
{#each line as elem} {#each line as elem}
<span class={elem.format}>
{#if elem && elem.type == 'img'} {#if elem && elem.type == 'img'}
{#if line.filter(x => x.type == 'img').length < 2} {#if line.filter(x => x.type == 'img').length < 2}
<img src={elem.url} class='only-img' alt='Image preview'> <img src={elem.url} class='only-img' alt='Image preview'>
@ -45,10 +55,11 @@
</video> </video>
{/if} {/if}
{:else if elem.type == 'link'} {:else if elem.type == 'link'}
<a href={elem.url}>{elem.display + ' '}</a> <a href={elem.url}>{elem.display}</a>
{:else if !elem.type} {:else if elem.type == 'text'}
{elem + ' '} {elem.content}
{/if} {/if}
</span>
{/each} {/each}
</p> </p>
{/each} {/each}

View file

@ -3,13 +3,15 @@
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import {setLocation} from '$lib/util.js'; import {setLocation} from '$lib/util.js';
export let data; export let data, noRatings;
</script> </script>
<p> <p>
{#if !noRatings}
<Button clickFunc={() => { window.location.search = setLocation(window.location,'sort','hot')}}>Hot</Button> <Button clickFunc={() => { window.location.search = setLocation(window.location,'sort','hot')}}>Hot</Button>
<Button clickFunc={() => { window.location.search = setLocation(window.location,'sort','rating')}}>Top</Button> <Button clickFunc={() => { window.location.search = setLocation(window.location,'sort','rating')}}>Top</Button>
<Button clickFunc={() => { window.location.search = setLocation(window.location,'sort','time')}}>Recent</Button> <Button clickFunc={() => { window.location.search = setLocation(window.location,'sort','time')}}>Recent</Button>
{/if}
</p> </p>
{#if data && data.postJson && data.postJson.data} {#if data && data.postJson && data.postJson.data}

View file

@ -83,6 +83,13 @@ async function initDb() {
username CHAR(64), \ username CHAR(64), \
following CHAR(64) \ following CHAR(64) \
)'); )');
await db.run('CREATE TABLE IF NOT EXISTS messages (\
username CHAR(64), \
content CHAR(10240), \
time INTEGER, \
read INTEGER \
)');
} }
let backendProxy = async ({route, backendParams}) => { let backendProxy = async ({route, backendParams}) => {
@ -99,7 +106,7 @@ let backendProxy = async ({route, backendParams}) => {
console.log(user); console.log(user);
if ((!user || user == '') && AUTH_ACTIONS.indexOf(route) != -1) return {'success': 'Not authorized.' }; if ((!user || user == '') && AUTH_ACTIONS.indexOf(route) != -1) return {'success': 'Not authorized.' };
3
let isAdmin = false; let isAdmin = false;
if (user && user != '') isAdmin = ((await backend.userRoles({user}, {db})) || []).indexOf('Admin') != -1; if (user && user != '') isAdmin = ((await backend.userRoles({user}, {db})) || []).indexOf('Admin') != -1;

View file

@ -202,9 +202,23 @@ backend.postCreate = async ({content}, {user,db}) => {
backend.postDelete = async ({id}, {user, admin, db}) => { backend.postDelete = async ({id}, {user, admin, db}) => {
if (admin) { if (admin) {
let postUser = await db.all('SELECT * from post where id = ?',[
id
]) || {};
postUser = postUser[0].username;
await db.run('DELETE FROM post WHERE id = ?', [ await db.run('DELETE FROM post WHERE id = ?', [
id id
]) ])
await db.run('INSERT INTO messages (username, content, time,read) VALUES (?, ?, ?, ?)', [
postUser,
`#${id} was deleted by an admin for rule violations.`,
Math.floor(new Date() * 1000),
0
]);
} else { } else {
await db.run('DELETE FROM post WHERE username = ? AND id = ?', [ await db.run('DELETE FROM post WHERE username = ? AND id = ?', [
user, user,
@ -248,7 +262,7 @@ backend.postBulk = async ({page, id, user, cookies, sort, type}, {admin, db}) =>
sort = (LEGAL_SORTS[sort]) || 'rating'; sort = (LEGAL_SORTS[sort]) || 'rating';
if (sort + '' != sort) sort = 'rating'; if (sort + '' !== sort) sort = 'rating';
sort = sort.replaceAll('%d',Math.floor(new Date() * 1000)); sort = sort.replaceAll('%d',Math.floor(new Date() * 1000));
@ -297,9 +311,9 @@ backend.vote = async ({id, vote}, {user, db}) => {
var isCreator = (await db.all('SELECT * from post WHERE id = ?', [ var isCreator = (await db.all('SELECT * from post WHERE id = ?', [
id id
]))[0].username == user; ]))[0].username;
if (isCreator) if (isCreator == user)
return {success: 'fail' }; return {success: 'fail' };
await db.run('DELETE FROM vote WHERE username = ? AND id = ?', [ await db.run('DELETE FROM vote WHERE username = ? AND id = ?', [
@ -327,6 +341,13 @@ backend.vote = async ({id, vote}, {user, db}) => {
id id
]); ]);
await db.run('INSERT INTO messages (username, content, time,read) VALUES (?, ?, ?, ?)', [
isCreator,
`@${user} ${vote == 'up' ? 'upvoted' : 'downvoted'} #${id}`,
Math.floor(new Date() * 1000),
0
]);
var user = await db.all('SELECT * from post WHERE id = ?', [ var user = await db.all('SELECT * from post WHERE id = ?', [
id id
]) || []; ]) || [];
@ -353,12 +374,20 @@ backend.token = async ({cookies}, {db}) => {
} }
backend.follow = async ({target}, {user, db}) => { backend.follow = async ({target}, {user, db}) => {
var userExists = ((await db.all('SELECT * FROM user WHERE username = ?',[
target
])) || []).length;
if (userExists < 1) return;
var isFollowing = await db.all('SELECT * FROM follow WHERE username = ? AND following = ?',[ var isFollowing = await db.all('SELECT * FROM follow WHERE username = ? AND following = ?',[
user, user,
target target
]); ]);
if (isFollowing && isFollowing.length > 0) { let unfollowed = (isFollowing && isFollowing.length > 0);
if (unfollowed) {
await db.run('DELETE FROM follow WHERE username = ? AND following = ?',[ await db.run('DELETE FROM follow WHERE username = ? AND following = ?',[
user, user,
target target
@ -378,9 +407,31 @@ backend.follow = async ({target}, {user, db}) => {
target target
]); ]);
await db.run('INSERT INTO messages (username, content, time,read) VALUES (?, ?, ?, ?)', [
target,
`@${user} ${unfollowed ? 'is now following' : 'unfollowed'} you`,
Math.floor(new Date() * 1000),
0
]);
return {'success': 'User followed/unfollowed.', 'data': {following, followers}}; return {'success': 'User followed/unfollowed.', 'data': {following, followers}};
}; };
backend.messages = async ({isRead}, {user, db}) => {
var msg = await db.all('SELECT * FROM messages WHERE username = ? ORDER BY time DESC', [
user
]) || [];
if (isRead) {
await db.run('UPDATE messages SET read = 1 WHERE username = ?', [
user
]);
}
let read = msg.filter(x => !x.read).length;
return {'data': {msg, read}};
};
export { export {

View file

@ -7,6 +7,12 @@ const EXTENSION_MAP = {
'mp4': 'video' 'mp4': 'video'
} }
const formats = [
'italic',
'bold',
'bolditalic'
];
let checkLength = function(string, field, lowerBound, upperBound) { let checkLength = function(string, field, lowerBound, upperBound) {
if (string.length < lowerBound) { if (string.length < lowerBound) {
if (string.length == 0) { if (string.length == 0) {
@ -66,11 +72,49 @@ let safeName = function (text) {
return text.replaceAll(/[^A-Za-z0-9\-\_]/g, ''); return text.replaceAll(/[^A-Za-z0-9\-\_]/g, '');
} }
let formatPost = function(post) { let formatPostText = function(post) {
post = post.map(x => (x === x + '') ? x.split(/(\*)/).filter(x => x != '') : x).flat();
let inc = false;
let formatType = -1;
let wasInc = false;
let dir = 1;
let newArr = [];
for (var i = 0; i < post.length; i++) {
inc = (post[i] == '*');
if (inc) {
formatType += dir;
} else {
if (wasInc) dir = !dir + 0;
if (dir) formatType = -1;
let formatTypeStr = formats[formatType] || 'default';
if (post[i] === post[i] + '') {
newArr.push({'type': 'text', 'format': formatTypeStr, 'content': post[i]})
} else {
let content = post[i];
content.format = formatTypeStr;
newArr.push(content);
}
}
wasInc = inc;
}
console.log(newArr);
return newArr;
}
let formatPost = function(post, ignoreImg) {
post = post.split('\n'); post = post.split('\n');
post = post.map(subPost => { post = post.map(subPost => {
return subPost.split(' '); return subPost.split(/( )/);
}); });
post = post.map(line => { post = post.map(line => {
@ -78,7 +122,7 @@ let formatPost = function(post) {
var splitPost = subPost.split('||'); var splitPost = subPost.split('||');
if (splitPost.length > 1) { if (splitPost.length > 1 && !ignoreImg) {
var cap1 = splitPost[0]; var cap1 = splitPost[0];
if (cap1 == 'img') { if (cap1 == 'img') {
@ -94,7 +138,7 @@ let formatPost = function(post) {
} else if (subPost[0] == '@' || subPost[0] == '#') { } else if (subPost[0] == '@' || subPost[0] == '#') {
var subPostIn = safeName(subPost.substring(0)); var subPostIn = safeName(subPost.substring(0));
var type = (subPost[0] == '@') ? 'user' : 'post'; var type = (subPost[0] == '@') ? 'users' : 'post';
splitPost = {'type': 'link', 'display': subPost, 'subtype': type, 'url': `/${type}/${subPostIn}`}; splitPost = {'type': 'link', 'display': subPost, 'subtype': type, 'url': `/${type}/${subPostIn}`};
@ -103,7 +147,7 @@ let formatPost = function(post) {
var url; var url;
var extension = subPost.split('.').pop().toLowerCase(); var extension = subPost.split('.').pop().toLowerCase();
if (EXTENSION_MAP[extension]) { if (EXTENSION_MAP[extension] && !ignoreImg) {
url = `/embed?url=${encodeURIComponent(subPost)}`; url = `/embed?url=${encodeURIComponent(subPost)}`;
splitPost = [ splitPost = [
{'type': 'link', 'display': subPost, 'url': url}, {'type': 'link', 'display': subPost, 'url': url},
@ -119,7 +163,7 @@ let formatPost = function(post) {
return subPost; return subPost;
}) })
return line.flat(); return formatPostText(line.flat());
}); });
return post; return post;

View file

@ -4,5 +4,8 @@ export async function load({ fetch }) {
const username = await res.json(); const username = await res.json();
return { username: username.data }; const res2 = await fetch(`/api/messages`);
const read = (await res2.json()).data.read;
return { username: username.data, read };
} }

View file

@ -0,0 +1,12 @@
import { redirect } from '@sveltejs/kit';
/** @type {import('./$types').PageLoad} */
export async function load({ fetch, url }) {
var page = url.searchParams.get('page') * 1;
await new Promise(resolve => setTimeout(resolve, 100));
const res = await fetch(`/api/messages?isRead=1`);
const postJson = (await res.json()).data.msg;
return {postJson, id: page};
}

View file

@ -0,0 +1,44 @@
<script>
import { setLocation} from '$lib/util.js';
import PostBody from '$lib/components/PostBody.svelte';
import Area from '$lib/components/Area.svelte';
/** @type {import('./$types').PageData} */
export let data;
</script>
<style>
.date {
font-size: 0.8rem;
font-style: italic;
font-weight: normal;
}
</style>
<Area handleSubmit=''>
<span slot="header">
Messages
</span>
<span slot='main'>
{#if data && data.postJson}
{#each data.postJson as post}
<PostBody content={post.content} excludeImg={true} />
<div class='date'>
{new Date(post.time / 1000)}
</div>
{/each}
{/if}
</span>
<p slot="footer">
Any messages and notifications sent to your profile.
</p>
</Area>
<p>
{#if data.id > 0}
<a on:click={() => { window.location.search = setLocation(window.location,'page',((data.id)-1)) }} href='#'>← Page {(data.id)-1}</a>
{/if}
<b>Page {(data.id)}</b>
<a on:click={() => { window.location.search = setLocation(window.location,'page',((data.id)+1)) }} href='#'>Page {(data.id)+1}</a>
</p>

View file

@ -82,7 +82,7 @@
<p>Create a post for the world to see.</p> <p>Create a post for the world to see.</p>
<h2>Post syntax</h2> <h2>Post syntax</h2>
<p> <p>
<b>img||filename.blah</b> embeds a user-uploaded file in this site <b>img||file.name</b> embeds a user-uploaded file in this site
</p> </p>
<p> <p>
<b>@user</b> mentions a user <b>@user</b> mentions a user
@ -90,5 +90,11 @@
<p> <p>
<b>#post</b> replies to a post by ID <b>#post</b> replies to a post by ID
</p> </p>
<p>
<b>*x*</b> produces italic text
</p>
<p>
<b>**x**</b> produces bolded text
</p>
</span> </span>
</Area> </Area>

66
static/read.svg Normal file
View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="read.svg"
inkscape:export-filename="pfp.png"
inkscape:export-xdpi="960.936"
inkscape:export-ydpi="960.936"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.72693604"
inkscape:cx="167.13988"
inkscape:cy="241.42427"
inkscape:window-width="1868"
inkscape:window-height="1006"
inkscape:window-x="24"
inkscape:window-y="46"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-26.001732,-18.540503)">
<circle
style="fill:#6da4ff;fill-rule:evenodd;stroke-width:2.64583;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers;fill-opacity:1"
id="path449"
cx="76.001732"
cy="68.540504"
r="50" />
<rect
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke-width:2.64583;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers"
id="rect1586"
width="64.08213"
height="39.604939"
x="43.960667"
y="48.738033"
ry="9.901206" />
<path
style="fill:none;stroke:#6da4ff;stroke-width:3.96875;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;stroke-dasharray:none"
d="M 43.960666,48.738033 76.001732,68.540503 108.0428,48.738033"
id="path1752" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

66
static/unread.svg Normal file
View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="unread.svg"
inkscape:export-filename="pfp.png"
inkscape:export-xdpi="960.936"
inkscape:export-ydpi="960.936"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.4538721"
inkscape:cx="50.210744"
inkscape:cy="240.39254"
inkscape:window-width="1868"
inkscape:window-height="1006"
inkscape:window-x="24"
inkscape:window-y="46"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-26.001732,-18.540503)">
<circle
style="fill:#ff6d6d;fill-rule:evenodd;stroke-width:2.64583;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers;fill-opacity:1"
id="path449"
cx="76.001732"
cy="68.540504"
r="50" />
<path
id="rect1059"
style="fill:#ffffff;fill-rule:evenodd;stroke-width:2.64583;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers"
d="M 56.89182,25.389756 H 95.111644 L 81.748186,84.382122 H 70.255277 Z"
sodipodi:nodetypes="ccccc" />
<ellipse
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke-width:2.64584;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers"
id="path1418"
cx="76.001732"
cy="99.319008"
rx="11.677505"
ry="11.018041" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB