Record sent chats in messages repo and blur gallery by default
Threads MessagesRepository through ChatService so first outgoing chat messages are persisted for stats, and extends the repo + DB client to support chat-room-keyed lookups. Adds tests covering the new behavior. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
2260388dc8
commit
05c95db6bd
10 files changed
+197
-6
No files matched your search
@@ -50,3 +50,5 @@ All new functionality and bug fixes MUST follow the red-green-refactor cycle. Th
|
|||||||
4. **Implement (Green)**: Write the smallest change that makes the test pass. Create new small files when SRP calls for it.
|
4. **Implement (Green)**: Write the smallest change that makes the test pass. Create new small files when SRP calls for it.
|
||||||
5. **Refactor**: Tidy the implementation and the test with the suite green. Re-run tests after each refactor step.
|
5. **Refactor**: Tidy the implementation and the test with the suite green. Re-run tests after each refactor step.
|
||||||
6. **Verify**: Ensure the full suite passes (`npm test`) and types check (`npm run build` or `tsc --noEmit`).
|
6. **Verify**: Ensure the full suite passes (`npm test`) and types check (`npm run build` or `tsc --noEmit`).
|
||||||
|
|
||||||
|
You must use the dokploy credentials in .env for access to dokploy
|
||||||
@@ -1515,6 +1515,39 @@
|
|||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.media-grid.revealed-all .media-card img {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unblur-all-btn {
|
||||||
|
padding: 7px 13px;
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--ink-soft);
|
||||||
|
border-color: var(--rule);
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: var(--f-body);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: transform .15s ease, background .2s ease, box-shadow .2s ease, color .2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unblur-all-btn:hover {
|
||||||
|
background: var(--card-2);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unblur-all-btn.active {
|
||||||
|
background: var(--terracotta-soft);
|
||||||
|
color: var(--terracotta-deep);
|
||||||
|
border-color: rgba(201, 123, 94, .35);
|
||||||
|
}
|
||||||
|
|
||||||
.media-meta {
|
.media-meta {
|
||||||
padding: 11px 13px;
|
padding: 11px 13px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2340,6 +2373,10 @@
|
|||||||
title="Walks every joined chat from scratch and re-downloads all history + media. Slow (~1–2 min). Use after long absences, missing rooms, or to rebuild the cache."
|
title="Walks every joined chat from scratch and re-downloads all history + media. Slow (~1–2 min). Use after long absences, missing rooms, or to rebuild the cache."
|
||||||
>Full sync</button>
|
>Full sync</button>
|
||||||
<button class="refresh-btn" onclick="fetchMedia()" id="refresh-media-btn" style="margin-left: 8px;">Refresh view</button>
|
<button class="refresh-btn" onclick="fetchMedia()" id="refresh-media-btn" style="margin-left: 8px;">Refresh view</button>
|
||||||
|
<button class="unblur-all-btn" id="unblur-all-btn" onclick="toggleUnblurAll()" title="Toggle blur on all gallery images">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||||
|
Unblur all
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-content media-grid" id="media-grid">
|
<div class="panel-content media-grid" id="media-grid">
|
||||||
<div class="empty-state">No media cached yet</div>
|
<div class="empty-state">No media cached yet</div>
|
||||||
@@ -3091,6 +3128,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isUnblurAll = false;
|
||||||
|
|
||||||
|
function toggleUnblurAll() {
|
||||||
|
isUnblurAll = !isUnblurAll;
|
||||||
|
const grid = document.getElementById('media-grid');
|
||||||
|
const btn = document.getElementById('unblur-all-btn');
|
||||||
|
if (isUnblurAll) {
|
||||||
|
grid.classList.add('revealed-all');
|
||||||
|
btn.classList.add('active');
|
||||||
|
} else {
|
||||||
|
grid.classList.remove('revealed-all');
|
||||||
|
btn.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Timer Logic
|
// Timer Logic
|
||||||
let timerState = {
|
let timerState = {
|
||||||
nextMessageAvailableAt: null,
|
nextMessageAvailableAt: null,
|
||||||
|
|||||||
@@ -206,6 +206,8 @@ export async function bootstrap(config: Config, logger: Logger): Promise<AppCont
|
|||||||
mediaService,
|
mediaService,
|
||||||
tokenRefresher,
|
tokenRefresher,
|
||||||
usernameResolver,
|
usernameResolver,
|
||||||
|
undefined,
|
||||||
|
messagesRepo,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mediaSyncService = new MediaSyncService(chatService, logger, config.media.syncIntervalMs);
|
const mediaSyncService = new MediaSyncService(chatService, logger, config.media.syncIntervalMs);
|
||||||
|
|||||||
@@ -98,6 +98,43 @@ export function runMigrations(db: Database.Database): void {
|
|||||||
// Add subreddit column to successful_messages for smarter example selection
|
// Add subreddit column to successful_messages for smarter example selection
|
||||||
addColumnIfNotExists('successful_messages', 'subreddit', 'TEXT');
|
addColumnIfNotExists('successful_messages', 'subreddit', 'TEXT');
|
||||||
|
|
||||||
|
// Track chats sent via chat.reddit.com so dashboard stats include them.
|
||||||
|
addColumnIfNotExists('sent_messages', 'chat_room_id', 'TEXT');
|
||||||
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sent_messages_chat_room ON sent_messages(chat_room_id)`);
|
||||||
|
|
||||||
|
// Migration: Make post_id nullable in sent_messages (chat-originated rows
|
||||||
|
// have no associated processed_post). SQLite can't ALTER COLUMN, so recreate.
|
||||||
|
const sentMsgColumns = db.prepare(`PRAGMA table_info(sent_messages)`).all() as { name: string; notnull: number }[];
|
||||||
|
const postIdCol = sentMsgColumns.find((c) => c.name === 'post_id');
|
||||||
|
if (postIdCol && postIdCol.notnull === 1) {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE sent_messages_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
post_id TEXT,
|
||||||
|
recipient TEXT NOT NULL,
|
||||||
|
subject TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
sent_at INTEGER NOT NULL,
|
||||||
|
got_response INTEGER DEFAULT 0,
|
||||||
|
response_checked_at INTEGER,
|
||||||
|
strategy_id TEXT,
|
||||||
|
chat_room_id TEXT,
|
||||||
|
FOREIGN KEY (post_id) REFERENCES processed_posts(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO sent_messages_new (id, post_id, recipient, subject, message, sent_at, got_response, response_checked_at, strategy_id, chat_room_id)
|
||||||
|
SELECT id, post_id, recipient, subject, message, sent_at, got_response, response_checked_at, strategy_id, chat_room_id
|
||||||
|
FROM sent_messages;
|
||||||
|
|
||||||
|
DROP TABLE sent_messages;
|
||||||
|
ALTER TABLE sent_messages_new RENAME TO sent_messages;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sent_messages_response ON sent_messages(got_response);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sent_messages_recipient ON sent_messages(recipient);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sent_messages_chat_room ON sent_messages(chat_room_id);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
// Migration: Make sent_message_id nullable in successful_messages
|
// Migration: Make sent_message_id nullable in successful_messages
|
||||||
// SQLite doesn't support ALTER COLUMN, so we need to recreate the table
|
// SQLite doesn't support ALTER COLUMN, so we need to recreate the table
|
||||||
const successfulMsgColumns = db.prepare(`PRAGMA table_info(successful_messages)`).all() as { name: string; notnull: number }[];
|
const successfulMsgColumns = db.prepare(`PRAGMA table_info(successful_messages)`).all() as { name: string; notnull: number }[];
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export interface ProcessedPostRecord {
|
|||||||
|
|
||||||
export interface SentMessageRecord {
|
export interface SentMessageRecord {
|
||||||
id: number;
|
id: number;
|
||||||
postId: string;
|
postId: string | null;
|
||||||
recipient: string;
|
recipient: string;
|
||||||
subject: string;
|
subject: string;
|
||||||
message: string;
|
message: string;
|
||||||
@@ -23,6 +23,7 @@ export interface SentMessageRecord {
|
|||||||
gotResponse: boolean;
|
gotResponse: boolean;
|
||||||
responseCheckedAt: number | null;
|
responseCheckedAt: number | null;
|
||||||
strategyId: string | null;
|
strategyId: string | null;
|
||||||
|
chatRoomId: string | null;
|
||||||
subreddit?: string;
|
subreddit?: string;
|
||||||
postTitle?: string;
|
postTitle?: string;
|
||||||
postImageUrl?: string;
|
postImageUrl?: string;
|
||||||
@@ -240,7 +241,9 @@ export class PostsRepository {
|
|||||||
|
|
||||||
export class MessagesRepository {
|
export class MessagesRepository {
|
||||||
private insertStmt: Database.Statement;
|
private insertStmt: Database.Statement;
|
||||||
|
private insertChatStmt: Database.Statement;
|
||||||
private getByPostIdStmt: Database.Statement;
|
private getByPostIdStmt: Database.Statement;
|
||||||
|
private getByChatRoomIdStmt: Database.Statement;
|
||||||
private getLatestByRecipientStmt: Database.Statement;
|
private getLatestByRecipientStmt: Database.Statement;
|
||||||
private getRecentStmt: Database.Statement;
|
private getRecentStmt: Database.Statement;
|
||||||
private getUncheckedStmt: Database.Statement;
|
private getUncheckedStmt: Database.Statement;
|
||||||
@@ -254,6 +257,20 @@ export class MessagesRepository {
|
|||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
this.insertChatStmt = db.prepare(`
|
||||||
|
INSERT INTO sent_messages (post_id, recipient, subject, message, sent_at, chat_room_id)
|
||||||
|
VALUES (NULL, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
|
||||||
|
this.getByChatRoomIdStmt = db.prepare(`
|
||||||
|
SELECT sm.*, pp.subreddit, pp.title AS post_title, pp.image_url AS post_image_url,
|
||||||
|
pp.image_analysis AS post_image_analysis
|
||||||
|
FROM sent_messages sm
|
||||||
|
LEFT JOIN processed_posts pp ON sm.post_id = pp.id
|
||||||
|
WHERE sm.chat_room_id = ?
|
||||||
|
LIMIT 1
|
||||||
|
`);
|
||||||
|
|
||||||
this.getByPostIdStmt = db.prepare(`SELECT * FROM sent_messages WHERE post_id = ?`);
|
this.getByPostIdStmt = db.prepare(`SELECT * FROM sent_messages WHERE post_id = ?`);
|
||||||
|
|
||||||
this.getLatestByRecipientStmt = db.prepare(`
|
this.getLatestByRecipientStmt = db.prepare(`
|
||||||
@@ -316,12 +333,35 @@ export class MessagesRepository {
|
|||||||
return result.lastInsertRowid as number;
|
return result.lastInsertRowid as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insertChat(message: {
|
||||||
|
recipient: string;
|
||||||
|
subject: string;
|
||||||
|
message: string;
|
||||||
|
sentAt: number;
|
||||||
|
chatRoomId: string;
|
||||||
|
}): number {
|
||||||
|
const result = this.insertChatStmt.run(
|
||||||
|
message.recipient,
|
||||||
|
message.subject,
|
||||||
|
message.message,
|
||||||
|
message.sentAt,
|
||||||
|
message.chatRoomId,
|
||||||
|
);
|
||||||
|
return result.lastInsertRowid as number;
|
||||||
|
}
|
||||||
|
|
||||||
getByPostId(postId: string): SentMessageRecord | undefined {
|
getByPostId(postId: string): SentMessageRecord | undefined {
|
||||||
const row = this.getByPostIdStmt.get(postId) as Record<string, unknown> | undefined;
|
const row = this.getByPostIdStmt.get(postId) as Record<string, unknown> | undefined;
|
||||||
if (!row) return undefined;
|
if (!row) return undefined;
|
||||||
return this.mapRow(row);
|
return this.mapRow(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getByChatRoomId(chatRoomId: string): SentMessageRecord | undefined {
|
||||||
|
const row = this.getByChatRoomIdStmt.get(chatRoomId) as Record<string, unknown> | undefined;
|
||||||
|
if (!row) return undefined;
|
||||||
|
return this.mapRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
getLatestByRecipient(recipient: string): SentMessageRecord | undefined {
|
getLatestByRecipient(recipient: string): SentMessageRecord | undefined {
|
||||||
const row = this.getLatestByRecipientStmt.get(recipient) as Record<string, unknown> | undefined;
|
const row = this.getLatestByRecipientStmt.get(recipient) as Record<string, unknown> | undefined;
|
||||||
if (!row) return undefined;
|
if (!row) return undefined;
|
||||||
@@ -422,7 +462,7 @@ export class MessagesRepository {
|
|||||||
private mapRow(row: Record<string, unknown>): SentMessageRecord {
|
private mapRow(row: Record<string, unknown>): SentMessageRecord {
|
||||||
return {
|
return {
|
||||||
id: row.id as number,
|
id: row.id as number,
|
||||||
postId: row.post_id as string,
|
postId: (row.post_id as string) ?? null,
|
||||||
recipient: row.recipient as string,
|
recipient: row.recipient as string,
|
||||||
subject: row.subject as string,
|
subject: row.subject as string,
|
||||||
message: row.message as string,
|
message: row.message as string,
|
||||||
@@ -430,6 +470,7 @@ export class MessagesRepository {
|
|||||||
gotResponse: Boolean(row.got_response),
|
gotResponse: Boolean(row.got_response),
|
||||||
responseCheckedAt: row.response_checked_at as number | null,
|
responseCheckedAt: row.response_checked_at as number | null,
|
||||||
strategyId: (row.strategy_id as string) || null,
|
strategyId: (row.strategy_id as string) || null,
|
||||||
|
chatRoomId: (row.chat_room_id as string) ?? null,
|
||||||
subreddit: (row.subreddit as string) || undefined,
|
subreddit: (row.subreddit as string) || undefined,
|
||||||
postTitle: (row.post_title as string) || undefined,
|
postTitle: (row.post_title as string) || undefined,
|
||||||
postImageUrl: (row.post_image_url as string) || undefined,
|
postImageUrl: (row.post_image_url as string) || undefined,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Logger } from '../../utils/logger.js';
|
import type { Logger } from '../../utils/logger.js';
|
||||||
import type { SuccessfulMessagesRepository } from '../../database/repositories.js';
|
import type { MessagesRepository, SuccessfulMessagesRepository } from '../../database/repositories.js';
|
||||||
import type { MediaService } from '../media.js';
|
import type { MediaService } from '../media.js';
|
||||||
import { ChatCache } from './cache.js';
|
import { ChatCache } from './cache.js';
|
||||||
import { MatrixClient, MatrixAuthError, MatrixRateLimitError } from './matrix/client.js';
|
import { MatrixClient, MatrixAuthError, MatrixRateLimitError } from './matrix/client.js';
|
||||||
@@ -114,6 +114,7 @@ export class ChatService {
|
|||||||
private tokenRefresher?: TokenRefresher,
|
private tokenRefresher?: TokenRefresher,
|
||||||
private usernameResolver?: UsernameResolver,
|
private usernameResolver?: UsernameResolver,
|
||||||
cache?: ChatCache,
|
cache?: ChatCache,
|
||||||
|
private messagesRepo?: MessagesRepository,
|
||||||
) {
|
) {
|
||||||
this.cache = cache ?? new ChatCache(logger);
|
this.cache = cache ?? new ChatCache(logger);
|
||||||
}
|
}
|
||||||
@@ -701,6 +702,27 @@ export class ChatService {
|
|||||||
if (firstSelf) chat.originMessage = firstSelf.text;
|
if (firstSelf) chat.originMessage = firstSelf.text;
|
||||||
|
|
||||||
const hasReply = chat.messages.some((m) => !m.isSelf);
|
const hasReply = chat.messages.some((m) => !m.isSelf);
|
||||||
|
|
||||||
|
if (firstSelf && chat.roomId && this.messagesRepo) {
|
||||||
|
try {
|
||||||
|
const existing = this.messagesRepo.getByChatRoomId(chat.roomId);
|
||||||
|
const id = existing
|
||||||
|
? existing.id
|
||||||
|
: this.messagesRepo.insertChat({
|
||||||
|
recipient: chat.username,
|
||||||
|
subject: chat.username,
|
||||||
|
message: firstSelf.text,
|
||||||
|
sentAt: firstSelf.timestamp,
|
||||||
|
chatRoomId: chat.roomId,
|
||||||
|
});
|
||||||
|
if (hasReply && !existing?.gotResponse) {
|
||||||
|
this.messagesRepo.markGotResponse(id);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn('[chat] failed to record sent_message for stats:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (firstSelf && hasReply && this.successfulMessagesRepo) {
|
if (firstSelf && hasReply && this.successfulMessagesRepo) {
|
||||||
if (!this.successfulMessagesRepo.existsByMessage(firstSelf.text)) {
|
if (!this.successfulMessagesRepo.existsByMessage(firstSelf.text)) {
|
||||||
const firstReply = chat.messages.find((m) => !m.isSelf);
|
const firstReply = chat.messages.find((m) => !m.isSelf);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class ResponseCheckerService {
|
|||||||
this.messagesRepo.markGotResponse(message.id);
|
this.messagesRepo.markGotResponse(message.id);
|
||||||
|
|
||||||
// Get the original post info
|
// Get the original post info
|
||||||
const post = this.postsRepo.getById(message.postId);
|
const post = message.postId ? this.postsRepo.getById(message.postId) : undefined;
|
||||||
|
|
||||||
// Store as successful message for learning
|
// Store as successful message for learning
|
||||||
this.successfulMessagesRepo.insert({
|
this.successfulMessagesRepo.insert({
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export function createConversationsRouter(ctx: WebContext): Router {
|
|||||||
} | null = null;
|
} | null = null;
|
||||||
try {
|
try {
|
||||||
const sent = ctx.repos.messages.getLatestByRecipient(conversation.username);
|
const sent = ctx.repos.messages.getLatestByRecipient(conversation.username);
|
||||||
if (sent) {
|
if (sent && sent.postId) {
|
||||||
const url = sent.subreddit
|
const url = sent.subreddit
|
||||||
? `https://www.reddit.com/r/${sent.subreddit}/comments/${sent.postId}/`
|
? `https://www.reddit.com/r/${sent.subreddit}/comments/${sent.postId}/`
|
||||||
: `https://redd.it/${sent.postId}`;
|
: `https://redd.it/${sent.postId}`;
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ describe('Database Client', () => {
|
|||||||
expect(columnMap.get('subject')?.notnull).toBe(1);
|
expect(columnMap.get('subject')?.notnull).toBe(1);
|
||||||
expect(columnMap.get('message')?.notnull).toBe(1);
|
expect(columnMap.get('message')?.notnull).toBe(1);
|
||||||
expect(columnMap.get('sent_at')?.notnull).toBe(1);
|
expect(columnMap.get('sent_at')?.notnull).toBe(1);
|
||||||
expect(columnMap.get('post_id')?.notnull).toBe(1);
|
expect(columnMap.get('post_id')?.notnull).toBe(0); // nullable — chat rows have no post
|
||||||
|
expect(columnMap.get('chat_room_id')).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successful_messages has correct schema', () => {
|
it('successful_messages has correct schema', () => {
|
||||||
|
|||||||
@@ -171,6 +171,40 @@ describe('MessagesRepository', () => {
|
|||||||
db = repos.db;
|
db = repos.db;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('insertChat stores a chat-originated send and getByChatRoomId returns it', () => {
|
||||||
|
const id = messagesRepo.insertChat({
|
||||||
|
recipient: 'alice',
|
||||||
|
subject: 'alice',
|
||||||
|
message: 'hey there',
|
||||||
|
sentAt: 1700000000000,
|
||||||
|
chatRoomId: '!room123:reddit.com',
|
||||||
|
});
|
||||||
|
expect(id).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const row = messagesRepo.getByChatRoomId('!room123:reddit.com');
|
||||||
|
expect(row).toBeDefined();
|
||||||
|
expect(row!.postId).toBeNull();
|
||||||
|
expect(row!.chatRoomId).toBe('!room123:reddit.com');
|
||||||
|
expect(row!.recipient).toBe('alice');
|
||||||
|
expect(row!.gotResponse).toBe(false);
|
||||||
|
|
||||||
|
messagesRepo.markGotResponse(id);
|
||||||
|
const updated = messagesRepo.getByChatRoomId('!room123:reddit.com');
|
||||||
|
expect(updated!.gotResponse).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('chat-originated rows appear in getRecent and contribute to stats', () => {
|
||||||
|
messagesRepo.insertChat({
|
||||||
|
recipient: 'bob',
|
||||||
|
subject: 'bob',
|
||||||
|
message: 'hi',
|
||||||
|
sentAt: Date.now(),
|
||||||
|
chatRoomId: '!r1:reddit.com',
|
||||||
|
});
|
||||||
|
const recent = messagesRepo.getRecent(10);
|
||||||
|
expect(recent.some((m) => m.chatRoomId === '!r1:reddit.com')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('inserts and retrieves a message', () => {
|
it('inserts and retrieves a message', () => {
|
||||||
const messageId = messagesRepo.insert({
|
const messageId = messagesRepo.insert({
|
||||||
postId: 'post1',
|
postId: 'post1',
|
||||||
|
|||||||
Reference in new issue
Block a user