feat: implement check functionality
This commit is contained in:
40
AGENTS.md
Normal file
40
AGENTS.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- `frontend-vue/` – Vue 3 + Vite app. Key folders: `src/components`, `src/views`, `src/stores`, `src/services`, `src/composables`, `src/router`, and static assets in `public/`.
|
||||
- `backend/` – TypeScript API + WebSocket server. Key folders: `src/controllers`, `src/routes`, `src/services`, `src/utils`, `src/logging`, `src/jobs`. Database files in `schema.sql` and `migrations/`.
|
||||
- `etc/systemd/` – example unit files for deployment. `dockerfile` – container build (optional).
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- Frontend
|
||||
- `cd frontend-vue && npm install`
|
||||
- `npm run dev` – start Vite dev server.
|
||||
- `npm run build` – type-check + production build.
|
||||
- `npm run preview` – preview production build.
|
||||
- `npm run lint` / `npm run format` – ESLint / Prettier.
|
||||
- Backend
|
||||
- `cd backend && npm install`
|
||||
- `npm run dev` – start server with `tsx --watch`.
|
||||
- `npm run start` – start server once (prod-like).
|
||||
- Note: backend was initialized with Bun; Node/npm + tsx is the primary flow.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- General: TypeScript, 2-space indent, small focused modules. File names: lowercase-kebab for modules (e.g., `message-service.ts`), `PascalCase.vue` for Vue SFCs.
|
||||
- Frontend: Prettier + ESLint enforced; prefer single quotes; composition API; component names in `PascalCase`.
|
||||
- Backend: Keep semicolons and consistent import quoting (matches current files). Use `PascalCase` for types/classes, `camelCase` for variables/functions.
|
||||
|
||||
## Testing Guidelines
|
||||
- No formal test runner is configured yet. If adding tests:
|
||||
- Place unit tests alongside code as `*.spec.ts` or in a `__tests__/` directory.
|
||||
- Keep tests fast and deterministic; document manual verification steps in PRs until a runner is introduced.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Commits: imperative mood, concise subject (<=72 chars), include scope prefix when helpful, e.g. `frontend: fix message input blur` or `backend: add channel search`.
|
||||
- PRs must include: clear description, linked issue (if any), screenshots/GIFs for UI changes, manual test steps, and notes on migrations if touching `backend/migrations/`.
|
||||
- Ensure `npm run lint` (frontend) passes and the app runs locally for both services before requesting review.
|
||||
|
||||
## Security & Configuration Tips
|
||||
- Backend reads environment from `.env` (see `backend/src/config.ts`): important keys include `DB_PATH`, `API_TOKEN`, `UPLOAD_DIR`, `PORT`, `USE_SSL`, `OPENAI_API_KEY`, `OLLAMA_URL`, and related model settings.
|
||||
- Do not commit secrets. Provide `.env` examples in PRs when adding new variables.
|
||||
- For local SSL, set `USE_SSL=1` and supply `SSL_KEY/SSL_CERT`, or let the server generate a self-signed pair for development.
|
||||
|
||||
3
backend/migrations/3_checked.sql
Normal file
3
backend/migrations/3_checked.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- Add tri-state checked column to messages (NULL | 0 | 1)
|
||||
ALTER TABLE messages ADD COLUMN checked INTEGER NULL;
|
||||
|
||||
@@ -13,19 +13,20 @@ CREATE TABLE IF NOT EXISTS files (
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (channelId) REFERENCES channels (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
channelId INTEGER,
|
||||
content TEXT,
|
||||
fileId INTEGER NULL,
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (channelId) REFERENCES channels (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (fileId) REFERENCES files (id) ON DELETE
|
||||
SET
|
||||
NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
channelId INTEGER,
|
||||
content TEXT,
|
||||
fileId INTEGER NULL,
|
||||
checked INTEGER NULL,
|
||||
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (channelId) REFERENCES channels (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (fileId) REFERENCES files (id) ON DELETE
|
||||
SET
|
||||
NULL
|
||||
);
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
||||
content,
|
||||
content = 'messages',
|
||||
content_rowid = 'id'
|
||||
);
|
||||
);
|
||||
|
||||
@@ -53,7 +53,7 @@ export const getMessages = async (req: Request, res: Response) => {
|
||||
res.json({ messages });
|
||||
}
|
||||
|
||||
export const moveMessage = async (req: Request, res: Response) => {
|
||||
export const moveMessage = async (req: Request, res: Response) => {
|
||||
const { messageId } = req.params;
|
||||
const { targetChannelId } = req.body;
|
||||
|
||||
@@ -77,4 +77,19 @@ export const moveMessage = async (req: Request, res: Response) => {
|
||||
logger.critical(`Failed to move message ${messageId}:`, error);
|
||||
res.status(500).json({ error: 'Failed to move message' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const setChecked = async (req: Request, res: Response) => {
|
||||
const { messageId } = req.params;
|
||||
const { checked } = req.body as { checked: boolean | null | undefined };
|
||||
if (!messageId) {
|
||||
return res.status(400).json({ error: 'Message ID is required' });
|
||||
}
|
||||
const value = (checked === undefined) ? null : checked;
|
||||
const result = await MessageService.setMessageChecked(messageId, value);
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ error: 'Message not found' });
|
||||
}
|
||||
logger.info(`Message ${messageId} checked set to ${value}`);
|
||||
res.json({ id: parseInt(messageId), checked: value });
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import { authenticate } from '../middleware/auth';
|
||||
|
||||
export const router = Router({mergeParams: true});
|
||||
|
||||
router.post('/', authenticate, MessageController.createMessage);
|
||||
router.put('/:messageId', authenticate, MessageController.updateMessage);
|
||||
router.put('/:messageId/move', authenticate, MessageController.moveMessage);
|
||||
router.delete('/:messageId', authenticate, MessageController.deleteMessage);
|
||||
router.get('/', authenticate, MessageController.getMessages);
|
||||
router.post('/', authenticate, MessageController.createMessage);
|
||||
router.put('/:messageId', authenticate, MessageController.updateMessage);
|
||||
router.put('/:messageId/move', authenticate, MessageController.moveMessage);
|
||||
router.put('/:messageId/checked', authenticate, MessageController.setChecked);
|
||||
router.delete('/:messageId', authenticate, MessageController.deleteMessage);
|
||||
router.get('/', authenticate, MessageController.getMessages);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { db, FTS5Enabled } from "../db";
|
||||
import { events } from "../globals";
|
||||
|
||||
export const createMessage = async (channelId: string, content: string) => {
|
||||
const query = db.prepare(`INSERT INTO messages (channelId, content) VALUES ($channelId, $content)`);
|
||||
const result = query.run({ channelId: channelId, content: content });
|
||||
export const createMessage = async (channelId: string, content: string) => {
|
||||
const query = db.prepare(`INSERT INTO messages (channelId, content, checked) VALUES ($channelId, $content, NULL)`);
|
||||
const result = query.run({ channelId: channelId, content: content });
|
||||
|
||||
const messageId = result.lastInsertRowid;
|
||||
console.log(`Adding message for search with id ${messageId}`);
|
||||
@@ -17,9 +17,9 @@ export const createMessage = async (channelId: string, content: string) => {
|
||||
return messageId;
|
||||
}
|
||||
|
||||
export const updateMessage = async (messageId: string, content: string, append: boolean = false) => {
|
||||
const query = db.prepare(`UPDATE messages SET content = $content WHERE id = $id`);
|
||||
const result = query.run({ content: content, id: messageId });
|
||||
export const updateMessage = async (messageId: string, content: string, append: boolean = false) => {
|
||||
const query = db.prepare(`UPDATE messages SET content = $content WHERE id = $id`);
|
||||
const result = query.run({ content: content, id: messageId });
|
||||
|
||||
|
||||
|
||||
@@ -46,13 +46,13 @@ export const deleteMessage = async (messageId: string) => {
|
||||
return result;
|
||||
}
|
||||
|
||||
export const getMessages = async (channelId: string) => {
|
||||
const query = db.prepare(`
|
||||
SELECT
|
||||
messages.id, messages.channelId, messages.content, messages.createdAt,
|
||||
files.id as fileId, files.filePath, files.fileType, files.createdAt as fileCreatedAt, files.originalName, files.fileSize
|
||||
FROM
|
||||
messages
|
||||
export const getMessages = async (channelId: string) => {
|
||||
const query = db.prepare(`
|
||||
SELECT
|
||||
messages.id, messages.channelId, messages.content, messages.createdAt, messages.checked,
|
||||
files.id as fileId, files.filePath, files.fileType, files.createdAt as fileCreatedAt, files.originalName, files.fileSize
|
||||
FROM
|
||||
messages
|
||||
LEFT JOIN
|
||||
files
|
||||
ON
|
||||
@@ -61,16 +61,16 @@ export const getMessages = async (channelId: string) => {
|
||||
messages.channelId = $channelId
|
||||
`);
|
||||
const rows = query.all({ channelId: channelId });
|
||||
return rows;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
export const getMessage = async (id: string) => {
|
||||
const query = db.prepare(`
|
||||
SELECT
|
||||
messages.id, messages.channelId, messages.content, messages.createdAt,
|
||||
files.id as fileId, files.filePath, files.fileType, files.createdAt as fileCreatedAt, files.originalName, files.fileSize
|
||||
FROM
|
||||
messages
|
||||
export const getMessage = async (id: string) => {
|
||||
const query = db.prepare(`
|
||||
SELECT
|
||||
messages.id, messages.channelId, messages.content, messages.createdAt, messages.checked,
|
||||
files.id as fileId, files.filePath, files.fileType, files.createdAt as fileCreatedAt, files.originalName, files.fileSize
|
||||
FROM
|
||||
messages
|
||||
LEFT JOIN
|
||||
files
|
||||
ON
|
||||
@@ -79,8 +79,17 @@ export const getMessage = async (id: string) => {
|
||||
messages.id = $id
|
||||
`);
|
||||
const row = query.get({ id: id });
|
||||
return row;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
export const setMessageChecked = async (messageId: string, checked: boolean | null) => {
|
||||
const query = db.prepare(`UPDATE messages SET checked = $checked WHERE id = $id`);
|
||||
// SQLite stores booleans as integers; NULL for unknown
|
||||
const value = checked === null ? null : (checked ? 1 : 0);
|
||||
const result = query.run({ id: messageId, checked: value });
|
||||
events.emit('message-updated', messageId, { checked: value });
|
||||
return result;
|
||||
}
|
||||
|
||||
export const moveMessage = async (messageId: string, targetChannelId: string) => {
|
||||
// Get current message to emit proper events
|
||||
@@ -104,4 +113,4 @@ export const moveMessage = async (messageId: string, targetChannelId: string) =>
|
||||
|
||||
events.emit('message-moved', messageId, (currentMessage as any).channelId, targetChannelId);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ export interface Channel {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: number;
|
||||
channel_id: number;
|
||||
content: string;
|
||||
created_at: string;
|
||||
}
|
||||
export interface Message {
|
||||
id: number;
|
||||
channel_id: number;
|
||||
content: string;
|
||||
created_at: string;
|
||||
checked?: boolean | null;
|
||||
}
|
||||
|
||||
export interface File {
|
||||
id: number;
|
||||
@@ -18,4 +19,4 @@ export interface File {
|
||||
file_path: string;
|
||||
file_type: string;
|
||||
created_at: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,16 @@
|
||||
🎤
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
@click="$emit('toggle-check')"
|
||||
aria-label="Toggle check on focused message"
|
||||
:disabled="disabled"
|
||||
>
|
||||
✓
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
variant="primary"
|
||||
size="sm"
|
||||
@@ -59,6 +69,7 @@ defineEmits<{
|
||||
'file-upload': []
|
||||
'camera': []
|
||||
'voice': []
|
||||
'toggle-check': []
|
||||
'send': []
|
||||
}>()
|
||||
</script>
|
||||
@@ -70,4 +81,10 @@ defineEmits<{
|
||||
gap: 0.25rem; /* Reduced gap to save space */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
/* Mobile-only for the checked toggle button */
|
||||
.input-actions [aria-label="Toggle check on focused message"] { display: none; }
|
||||
@media (max-width: 480px) {
|
||||
.input-actions [aria-label="Toggle check on focused message"] { display: inline-flex; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
@file-upload="$emit('file-upload')"
|
||||
@camera="$emit('camera')"
|
||||
@voice="$emit('voice')"
|
||||
@toggle-check="$emit('toggle-check')"
|
||||
@send="handleSubmit"
|
||||
/>
|
||||
</div>
|
||||
@@ -35,6 +36,7 @@ const emit = defineEmits<{
|
||||
'file-upload': []
|
||||
'camera': []
|
||||
'voice': []
|
||||
'toggle-check': []
|
||||
}>()
|
||||
|
||||
const appStore = useAppStore()
|
||||
@@ -120,4 +122,4 @@ defineExpose({
|
||||
border-top-color: #374151;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
@focus="handleFocus"
|
||||
>
|
||||
<div class="message__content">
|
||||
<span v-if="isChecked === true" class="message__check" aria-hidden="true">✔</span>
|
||||
<span v-else-if="isChecked === false" class="message__check message__check--unchecked" aria-hidden="true">☐</span>
|
||||
{{ message.content }}
|
||||
</div>
|
||||
|
||||
@@ -23,6 +25,16 @@
|
||||
</div>
|
||||
|
||||
<div class="message__meta">
|
||||
<button
|
||||
class="message__toggle"
|
||||
type="button"
|
||||
:aria-label="toggleAriaLabel"
|
||||
@click.stop="toggleChecked()"
|
||||
>
|
||||
<span v-if="isChecked === true">Uncheck</span>
|
||||
<span v-else-if="isChecked === false">Check</span>
|
||||
<span v-else>Check</span>
|
||||
</button>
|
||||
<time
|
||||
v-if="!isUnsent && 'created_at' in message"
|
||||
class="message__time"
|
||||
@@ -83,6 +95,11 @@ const hasFileAttachment = computed(() => {
|
||||
return 'fileId' in props.message && !!props.message.fileId
|
||||
})
|
||||
|
||||
// Tri-state checked
|
||||
const isChecked = computed<boolean | null>(() => {
|
||||
return (props as any).message?.checked ?? null
|
||||
})
|
||||
|
||||
// Create FileAttachment object from flattened message data
|
||||
const fileAttachment = computed((): FileAttachmentType | null => {
|
||||
if (!hasFileAttachment.value || !('fileId' in props.message)) return null
|
||||
@@ -114,7 +131,15 @@ const fileAttachment = computed((): FileAttachmentType | null => {
|
||||
|
||||
// Create comprehensive aria-label for screen readers
|
||||
const messageAriaLabel = computed(() => {
|
||||
let prefix = ''
|
||||
let label = ''
|
||||
|
||||
// Checked state first
|
||||
if ((props as any).message?.checked === true) {
|
||||
prefix = 'checked, '
|
||||
} else if ((props as any).message?.checked === false) {
|
||||
prefix = 'unchecked, '
|
||||
}
|
||||
|
||||
// Add message content
|
||||
if (props.message.content) {
|
||||
@@ -139,7 +164,7 @@ const messageAriaLabel = computed(() => {
|
||||
label += '. Message is sending'
|
||||
}
|
||||
|
||||
return label
|
||||
return `${prefix}${label}`.trim()
|
||||
})
|
||||
|
||||
// Helper to determine file type for better description
|
||||
@@ -174,6 +199,12 @@ const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) {
|
||||
return
|
||||
}
|
||||
if (event.key === ' ' || event.code === 'Space') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
toggleChecked()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'c') {
|
||||
// Copy message content (only when no modifiers are pressed)
|
||||
@@ -268,6 +299,33 @@ const handleDelete = async () => {
|
||||
toastStore.error('Failed to delete message')
|
||||
}
|
||||
}
|
||||
const handleFocus = () => {
|
||||
// Keep parent selection index in sync
|
||||
emit('focus')
|
||||
}
|
||||
|
||||
const toggleAriaLabel = computed(() => {
|
||||
if (isChecked.value === true) return 'Mark as unchecked'
|
||||
if (isChecked.value === false) return 'Mark as checked'
|
||||
return 'Mark as checked'
|
||||
})
|
||||
|
||||
const toggleChecked = async () => {
|
||||
if (props.isUnsent) return
|
||||
const msg = props.message as ExtendedMessage
|
||||
const next = isChecked.value !== true
|
||||
const prev = isChecked.value
|
||||
try {
|
||||
// optimistic
|
||||
appStore.setMessageChecked(msg.id, next)
|
||||
await apiService.setMessageChecked(msg.channel_id, msg.id, next)
|
||||
} catch (e) {
|
||||
// rollback
|
||||
appStore.setMessageChecked(msg.id, prev as any)
|
||||
console.error('Failed to set checked state', e)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -330,6 +388,31 @@ const handleDelete = async () => {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message__check {
|
||||
margin-right: 6px;
|
||||
color: #059669;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message__check--unchecked {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.message__toggle {
|
||||
appearance: none;
|
||||
border: 1px solid #d1d5db;
|
||||
background: #fff;
|
||||
color: #374151;
|
||||
border-radius: 6px;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
/* Hide the per-message toggle on desktop; show only on mobile */
|
||||
.message__toggle { display: none; }
|
||||
@media (max-width: 480px) {
|
||||
.message__toggle { display: inline-flex; }
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.message {
|
||||
background: #2d3748;
|
||||
@@ -352,7 +435,4 @@ const handleDelete = async () => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
const handleFocus = () => {
|
||||
// Emit a focus event so the parent list can update its focused index
|
||||
emit('focus')
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,13 @@ class ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async setMessageChecked(channelId: number, messageId: number, checked: boolean | null): Promise<{ id: number, checked: boolean | null }> {
|
||||
return this.request(`/channels/${channelId}/messages/${messageId}/checked`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ checked })
|
||||
})
|
||||
}
|
||||
|
||||
async moveMessage(channelId: number, messageId: number, targetChannelId: number): Promise<{ message: string, messageId: number, targetChannelId: number }> {
|
||||
return this.request(`/channels/${channelId}/messages/${messageId}/move`, {
|
||||
method: 'PUT',
|
||||
@@ -162,4 +169,4 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
export const apiService = new ApiService()
|
||||
export const apiService = new ApiService()
|
||||
|
||||
@@ -35,6 +35,7 @@ export class SyncService {
|
||||
content: msg.content,
|
||||
created_at: msg.createdAt || msg.created_at,
|
||||
file_id: msg.fileId || msg.file_id,
|
||||
checked: typeof msg.checked === 'number' ? (msg.checked === 1) : (typeof msg.checked === 'boolean' ? msg.checked : null),
|
||||
// Map the flattened file fields from backend
|
||||
fileId: msg.fileId,
|
||||
filePath: msg.filePath,
|
||||
|
||||
@@ -101,6 +101,10 @@ export const useAppStore = defineStore('app', () => {
|
||||
}
|
||||
}
|
||||
|
||||
const setMessageChecked = (messageId: number, checked: boolean | null) => {
|
||||
updateMessage(messageId, { checked })
|
||||
}
|
||||
|
||||
const removeMessage = (messageId: number) => {
|
||||
for (const channelId in messages.value) {
|
||||
const channelMessages = messages.value[parseInt(channelId)]
|
||||
@@ -210,6 +214,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
setMessages,
|
||||
addMessage,
|
||||
updateMessage,
|
||||
setMessageChecked,
|
||||
removeMessage,
|
||||
moveMessage,
|
||||
addUnsentMessage,
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface Message {
|
||||
content: string
|
||||
created_at: string
|
||||
file_id?: number
|
||||
checked?: boolean | null
|
||||
}
|
||||
|
||||
export interface MessageWithFile extends Message {
|
||||
@@ -130,4 +131,4 @@ export interface UploadProgress {
|
||||
loaded: number
|
||||
total: number
|
||||
percentage: number
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
@file-upload="showFileDialog = true"
|
||||
@camera="showCameraDialog = true"
|
||||
@voice="showVoiceDialog = true"
|
||||
@toggle-check="handleToggleCheckFocused"
|
||||
ref="messageInput"
|
||||
/>
|
||||
</div>
|
||||
@@ -329,6 +330,19 @@ const setupKeyboardShortcuts = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleToggleCheckFocused = async () => {
|
||||
const focused = messagesContainer.value?.getFocusedMessage?.()
|
||||
if (!focused || 'channelId' in focused) return
|
||||
try {
|
||||
const next = (focused as ExtendedMessage).checked !== true
|
||||
appStore.setMessageChecked((focused as ExtendedMessage).id, next)
|
||||
await apiService.setMessageChecked((focused as ExtendedMessage).channel_id, (focused as ExtendedMessage).id, next)
|
||||
toastStore.info(next ? 'Marked as checked' : 'Marked as unchecked')
|
||||
} catch (e) {
|
||||
toastStore.error('Failed to toggle check')
|
||||
}
|
||||
}
|
||||
|
||||
const selectChannel = async (channelId: number) => {
|
||||
console.log('Selecting channel:', channelId)
|
||||
await appStore.setCurrentChannel(channelId)
|
||||
@@ -763,4 +777,4 @@ onMounted(async () => {
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user