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='/'>
<img src='/icon_sanifae.svg' alt='Sanifae Logo'>
</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'}
<a href='/users/{data.username}'>
{data.username}

View file

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

View file

@ -3,13 +3,15 @@
import Button from '$lib/components/Button.svelte';
import {setLocation} from '$lib/util.js';
export let data;
export let data, noRatings;
</script>
<p>
<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','time')}}>Recent</Button>
{#if !noRatings}
<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','time')}}>Recent</Button>
{/if}
</p>
{#if data && data.postJson && data.postJson.data}

View file

@ -83,6 +83,13 @@ async function initDb() {
username 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}) => {
@ -99,7 +106,7 @@ let backendProxy = async ({route, backendParams}) => {
console.log(user);
if ((!user || user == '') && AUTH_ACTIONS.indexOf(route) != -1) return {'success': 'Not authorized.' };
3
let isAdmin = false;
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}) => {
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 = ?', [
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 {
await db.run('DELETE FROM post WHERE username = ? AND id = ?', [
user,
@ -248,7 +262,7 @@ backend.postBulk = async ({page, id, user, cookies, sort, type}, {admin, db}) =>
sort = (LEGAL_SORTS[sort]) || 'rating';
if (sort + '' != sort) sort = 'rating';
if (sort + '' !== sort) sort = 'rating';
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 = ?', [
id
]))[0].username == user;
]))[0].username;
if (isCreator)
if (isCreator == user)
return {success: 'fail' };
await db.run('DELETE FROM vote WHERE username = ? AND id = ?', [
@ -327,6 +341,13 @@ backend.vote = async ({id, vote}, {user, db}) => {
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 = ?', [
id
]) || [];
@ -353,12 +374,20 @@ backend.token = async ({cookies}, {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 = ?',[
user,
target
]);
if (isFollowing && isFollowing.length > 0) {
let unfollowed = (isFollowing && isFollowing.length > 0);
if (unfollowed) {
await db.run('DELETE FROM follow WHERE username = ? AND following = ?',[
user,
target
@ -378,9 +407,31 @@ backend.follow = async ({target}, {user, db}) => {
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}};
};
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 {

View file

@ -7,6 +7,12 @@ const EXTENSION_MAP = {
'mp4': 'video'
}
const formats = [
'italic',
'bold',
'bolditalic'
];
let checkLength = function(string, field, lowerBound, upperBound) {
if (string.length < lowerBound) {
if (string.length == 0) {
@ -66,11 +72,49 @@ let safeName = function (text) {
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.map(subPost => {
return subPost.split(' ');
return subPost.split(/( )/);
});
post = post.map(line => {
@ -78,7 +122,7 @@ let formatPost = function(post) {
var splitPost = subPost.split('||');
if (splitPost.length > 1) {
if (splitPost.length > 1 && !ignoreImg) {
var cap1 = splitPost[0];
if (cap1 == 'img') {
@ -94,7 +138,7 @@ let formatPost = function(post) {
} else if (subPost[0] == '@' || subPost[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}`};
@ -103,7 +147,7 @@ let formatPost = function(post) {
var url;
var extension = subPost.split('.').pop().toLowerCase();
if (EXTENSION_MAP[extension]) {
if (EXTENSION_MAP[extension] && !ignoreImg) {
url = `/embed?url=${encodeURIComponent(subPost)}`;
splitPost = [
{'type': 'link', 'display': subPost, 'url': url},
@ -119,7 +163,7 @@ let formatPost = function(post) {
return subPost;
})
return line.flat();
return formatPostText(line.flat());
});
return post;

View file

@ -4,5 +4,8 @@ export async function load({ fetch }) {
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>
<h2>Post syntax</h2>
<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>
<b>@user</b> mentions a user
@ -90,5 +90,11 @@
<p>
<b>#post</b> replies to a post by ID
</p>
<p>
<b>*x*</b> produces italic text
</p>
<p>
<b>**x**</b> produces bolded text
</p>
</span>
</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