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
No known key found for this signature in database
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,20 +1,23 @@
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( CREATE TABLE IF NOT EXISTS users(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
username VARCHAR(32) UNIQUE NOT NULL, username VARCHAR(32) UNIQUE NOT NULL,
password TEXT, password TEXT,
is_superuser BOOLEAN is_superuser BOOLEAN
); );
`,
`
CREATE TABLE IF NOT EXISTS channels( CREATE TABLE IF NOT EXISTS channels(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name VARCHAR(32) NOT NULL, name VARCHAR(32) NOT NULL,
owner_id SERIAL REFERENCES users ON DELETE CASCADE owner_id SERIAL REFERENCES users ON DELETE CASCADE
); );
`,
`
CREATE TABLE IF NOT EXISTS messages( CREATE TABLE IF NOT EXISTS messages(
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
content VARCHAR(4000) NOT NULL, content VARCHAR(4000) NOT NULL,
@ -22,5 +25,13 @@ export default async function databaseInit() {
author_id SERIAL REFERENCES users ON DELETE CASCADE, author_id SERIAL REFERENCES users ON DELETE CASCADE,
created_at BIGINT created_at BIGINT
); );
`); `,
`
ALTER TABLE messages ADD COLUMN nick_username VARCHAR(64) DEFAULT '';
`
];
for (let i = 0; i < migrations.length; i++) {
await query(migrations[i], [], false);
}
} }

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",