add "nick_username" feature for messages - useful for bridges to other chat platforms

This commit is contained in:
hippoz 2022-10-30 16:53:52 +02:00
parent 53689fdaf6
commit 3ef8298745
Signed by: hippoz
GPG key ID: 7C52899193467641
9 changed files with 87 additions and 50 deletions

View file

@ -387,6 +387,23 @@ body {
user-select: none; user-select: none;
} }
/* badges */
.user-badge {
display: inline-flex;
justify-content: center;
align-items: center;
background-color: var(--purple-2);
padding-top: 1px;
padding-bottom: 1px;
padding-left: 0.375rem;
padding-right: 0.375rem;
border-radius: 9999px;
font-size: x-small;
margin-left: var(--space-sm);
cursor: pointer;
}
/*! the tweaks below are heavily based on modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */ /*! the tweaks below are heavily based on modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */
*, *,

View file

@ -53,11 +53,18 @@
} }
.author { .author {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0; flex-shrink: 0;
font-weight: bold; font-weight: bold;
margin-right: var(--space-xs); margin-right: var(--space-xs);
} }
.author-more {
margin-right: 0;
}
.edit-message { .edit-message {
flex-shrink: 0; flex-shrink: 0;
float: right; float: right;
@ -106,10 +113,21 @@
.message.clumped { .message.clumped {
padding: 2px 2px 2px var(--space-normplus); padding: 2px 2px 2px var(--space-normplus);
} }
.via-badge {
margin-left: var(--space-xs);
margin-right: var(--space-md);
cursor: default;
}
</style> </style>
<div class="message" class:clumped={ message._clumped } class:pinged={ message._mentions }> <div class="message" class:clumped={ message._clumped } class:pinged={ message._mentions }>
<span class="author">{ message.author_username }</span> <span class="author" class:author-more={message._viaBadge}>
{ message._effectiveAuthor }
</span>
{#if message._viaBadge}
<span class="user-badge via-badge">via { message._viaBadge }</span>
{/if}
<span class="message-content" class:pending={ message._isPending }>{ message.content }</span> <span class="message-content" class:pending={ message._isPending }>{ message.content }</span>
<span class="date message-extra">{ message._createdAtTimeString }</span> <span class="date message-extra">{ message._createdAtTimeString }</span>

View file

@ -9,23 +9,6 @@
}; };
</script> </script>
<style>
.user-badge {
display: inline-flex;
justify-content: center;
align-items: center;
background-color: var(--purple-2);
padding-top: 1px;
padding-bottom: 1px;
padding-left: 0.375rem;
padding-right: 0.375rem;
border-radius: 9999px;
font-size: x-small;
margin-left: var(--space-sm);
cursor: pointer;
}
</style>
<div class="sidebar-container" in:maybeFly="{{ duration: 175, easing: quadInOut, x: 10 }}" out:maybeFlyIf="{{ _condition: !smallViewport.value, duration: 175, easing: quadInOut, x: -10 }}"> <div class="sidebar-container" in:maybeFly="{{ duration: 175, easing: quadInOut, x: 10 }}" out:maybeFlyIf="{{ _condition: !smallViewport.value, duration: 175, easing: quadInOut, x: -10 }}">
<div class="top-bar"> <div class="top-bar">
<span class="input-label">User List</span> <span class="input-label">User List</span>

View file

@ -233,13 +233,19 @@ class MessageStore extends Store {
message._editable = false; message._editable = false;
message._clumped = false; message._clumped = false;
message._aboveDateMarker = null; message._aboveDateMarker = null;
message._viaBadge = null;
message._effectiveAuthor = message.author_username;
if (message.nick_username && message.nick_username !== "") {
message._effectiveAuthor = message.nick_username;
message._viaBadge = message.author_username;
}
if (userInfoStore.value && message.content.includes("@" + userInfoStore.value.username)) { if (userInfoStore.value && message.content.includes("@" + userInfoStore.value.username)) {
message._mentions = true; message._mentions = true;
} }
if (userInfoStore.value && (message.author_id === userInfoStore.value.id || userInfoStore.value.is_superuser)) { if (userInfoStore.value && (message.author_id === userInfoStore.value.id || userInfoStore.value.is_superuser)) {
message._editable = true; message._editable = true;
} }
if (previous && (message._createdAtDate.getTime() - previous._createdAtDate.getTime()) <= 100 * 1000 && message.author_id === previous.author_id) { if (previous && (message._createdAtDate.getTime() - previous._createdAtDate.getTime()) <= 100 * 1000 && message.author_id === previous.author_id && message._effectiveAuthor === previous._effectiveAuthor && message._viaBadge === previous._viaBadge) {
message._clumped = true; message._clumped = true;
} }
if (!previous || (previous._createdAtDateString !== message._createdAtDateString && !message._aboveDateMarker)) { if (!previous || (previous._createdAtDateString !== message._createdAtDateString && !message._aboveDateMarker)) {

View file

@ -1,26 +1,37 @@
import { query } from "."; import { query, withClient } from ".";
export default async function databaseInit() { export default async function databaseInit() {
await query(` const migrations = [
CREATE TABLE IF NOT EXISTS users( `
id SERIAL PRIMARY KEY, CREATE TABLE IF NOT EXISTS users(
username VARCHAR(32) UNIQUE NOT NULL, id SERIAL PRIMARY KEY,
password TEXT, username VARCHAR(32) UNIQUE NOT NULL,
is_superuser BOOLEAN password TEXT,
); is_superuser BOOLEAN
);
`,
`
CREATE TABLE IF NOT EXISTS channels(
id SERIAL PRIMARY KEY,
name VARCHAR(32) NOT NULL,
owner_id SERIAL REFERENCES users ON DELETE CASCADE
);
`,
`
CREATE TABLE IF NOT EXISTS messages(
id SERIAL PRIMARY KEY,
content VARCHAR(4000) NOT NULL,
channel_id SERIAL REFERENCES channels ON DELETE CASCADE,
author_id SERIAL REFERENCES users ON DELETE CASCADE,
created_at BIGINT
);
`,
`
ALTER TABLE messages ADD COLUMN nick_username VARCHAR(64) DEFAULT '';
`
];
CREATE TABLE IF NOT EXISTS channels( for (let i = 0; i < migrations.length; i++) {
id SERIAL PRIMARY KEY, await query(migrations[i], [], false);
name VARCHAR(32) NOT NULL, }
owner_id SERIAL REFERENCES users ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS messages(
id SERIAL PRIMARY KEY,
content VARCHAR(4000) NOT NULL,
channel_id SERIAL REFERENCES channels ON DELETE CASCADE,
author_id SERIAL REFERENCES users ON DELETE CASCADE,
created_at BIGINT
);
`);
} }

View file

@ -1,4 +1,4 @@
export const getMessageById = "SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id = $1"; export const getMessageById = "SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, messages.nick_username, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id = $1";
export const getMessagesByChannelFirstPage = (limit: number) => `SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.channel_id = $1 ORDER BY id DESC LIMIT ${limit}`; export const getMessagesByChannelFirstPage = (limit: number) => `SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, messages.nick_username, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.channel_id = $1 ORDER BY id DESC LIMIT ${limit}`;
export const getMessagesByChannelPage = (limit: number) => `SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id < $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`; export const getMessagesByChannelPage = (limit: number) => `SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, messages.nick_username, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id < $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`;
export const getMessagesByChannelAfterPage = (limit: number) => `SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id > $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`; export const getMessagesByChannelAfterPage = (limit: number) => `SELECT messages.id, messages.content, messages.channel_id, messages.created_at, messages.author_id, messages.nick_username, users.username AS author_username FROM messages JOIN users ON messages.author_id = users.id WHERE messages.id > $1 AND messages.channel_id = $2 ORDER BY id DESC LIMIT ${limit}`;

View file

@ -2,11 +2,11 @@ import { query } from "./database";
import { dispatch } from "./gateway"; import { dispatch } from "./gateway";
import { GatewayPayloadType } from "./gateway/gatewaypayloadtype"; import { GatewayPayloadType } from "./gateway/gatewaypayloadtype";
export default async function sendMessage(user: User, channelId: number, optimisticId: number | null, content: string) { export default async function sendMessage(user: User, channelId: number, optimisticId: number | null, content: string, nickUsername: string | null) {
const authorId = user.id; const authorId = user.id;
const createdAt = Date.now().toString(); const createdAt = Date.now().toString();
const result = await query("INSERT INTO messages(content, channel_id, author_id, created_at) VALUES ($1, $2, $3, $4) RETURNING id", [content, channelId, authorId, createdAt]); const result = await query("INSERT INTO messages(content, channel_id, author_id, created_at, nick_username) VALUES ($1, $2, $3, $4, $5) RETURNING id", [content, channelId, authorId, createdAt, nickUsername]);
if (!result || result.rowCount < 1) { if (!result || result.rowCount < 1) {
return null; return null;
} }
@ -17,7 +17,8 @@ export default async function sendMessage(user: User, channelId: number, optimis
channel_id: channelId, channel_id: channelId,
author_id: authorId, author_id: authorId,
author_username: user.username, author_username: user.username,
created_at: createdAt created_at: createdAt,
nick_username: nickUsername
}; };
dispatch(`channel:${channelId}`, (ws) => { dispatch(`channel:${channelId}`, (ws) => {

View file

@ -175,13 +175,14 @@ router.post(
param("id").isNumeric(), param("id").isNumeric(),
body("content").isLength({ min: 1, max: 4000 }), body("content").isLength({ min: 1, max: 4000 }),
body("optimistic_id").optional().isNumeric(), body("optimistic_id").optional().isNumeric(),
body("nick_username").optional().isString().isLength({ min: 1, max: 64 }),
async (req, res) => { async (req, res) => {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) { if (!validationErrors.isEmpty()) {
return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() });
} }
return res.status(201).send(await sendMessage(req.user, parseInt(req.params.id), parseInt(req.body.optimistic_id), req.body.content)); return res.status(201).send(await sendMessage(req.user, parseInt(req.params.id), parseInt(req.body.optimistic_id), req.body.content, req.body.nick_username));
} }
); );

View file

@ -295,7 +295,7 @@ router.put(
error: "Message body must be a string between 1 and 2000 characters" error: "Message body must be a string between 1 and 2000 characters"
}); });
} }
const message = await sendMessage(req.user, channelId, null, req.body.body); const message = await sendMessage(req.user, channelId, null, req.body.body, null);
if (!message) { if (!message) {
return res.status(500).json({ return res.status(500).json({
errcode: "M_UNKNOWN", errcode: "M_UNKNOWN",