From 91c6f46fb47397a8d49d8111d393ccc19a5d04ac Mon Sep 17 00:00:00 2001 From: Fovway Date: Sat, 8 Nov 2025 23:49:00 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0?= =?UTF-8?q?=D0=BB=20=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80=D0=B8=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=BD=D0=BE=D1=81=D1=82=D1=8C=D1=8E=20=D0=B7=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D0=BE=D0=BA.=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D1=8B=20=D0=B8=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D0=B1=D0=B0=D0=B7?= =?UTF-8?q?=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=8F=20is=5Fprivate,=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20API=20=D0=B8?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D0=B8=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B8=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B7=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D0=BE=D0=BA=20=D1=81=20=D1=83=D1=87=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=20=D0=BF=D1=80=D0=B8=D0=B2=D0=B0=D1=82=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8.=20=D0=9F=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB?= =?UTF-8?q?=D1=87=D0=B0=D0=BD=D0=B8=D1=8E=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=BA=D0=B8=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D1=8E=D1=82=D1=81?= =?UTF-8?q?=D1=8F=20=D0=BA=D0=B0=D0=BA=20=D0=BF=D1=80=D0=B8=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=BD=D1=8B=D0=B5,=20=D0=B0=20=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B8=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B3=D1=83=D1=82=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D1=8F=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D0=B2=D0=B0=D1=82=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81.=20=D0=9E=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=82=D0=B8=D0=BF=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B8=20=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D1=85=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/server.js | 89 +++++++++++++++++++---- dev-dist/sw.js | 2 +- src/api/notesApi.ts | 5 +- src/api/offlineNotesApi.ts | 10 ++- src/components/notes/NoteEditor.tsx | 20 +++++- src/components/notes/NoteItem.tsx | 107 ++++++++++++++++++++++++---- src/services/syncService.ts | 2 + src/types/note.ts | 1 + 8 files changed, 203 insertions(+), 33 deletions(-) diff --git a/backend/server.js b/backend/server.js index 5af3eee..9cdc5d2 100644 --- a/backend/server.js +++ b/backend/server.js @@ -614,7 +614,7 @@ function runMigrations() { ); if (!hasIsPublicProfile) { db.run( - "ALTER TABLE users ADD COLUMN is_public_profile INTEGER DEFAULT 0", + "ALTER TABLE users ADD COLUMN is_public_profile INTEGER DEFAULT 1", (err) => { if (err) { console.error( @@ -625,6 +625,39 @@ function runMigrations() { console.log( "Колонка is_public_profile добавлена в таблицу users" ); + // Устанавливаем is_public_profile = 1 для всех существующих пользователей + db.run( + "UPDATE users SET is_public_profile = 1 WHERE is_public_profile IS NULL OR is_public_profile = 0", + (updateErr) => { + if (updateErr) { + console.error( + "Ошибка обновления is_public_profile для существующих пользователей:", + updateErr.message + ); + } else { + console.log( + "is_public_profile установлен в 1 для всех существующих пользователей" + ); + } + } + ); + } + } + ); + } else { + // Если колонка уже существует, обновляем существующих пользователей с NULL или 0 + db.run( + "UPDATE users SET is_public_profile = 1 WHERE is_public_profile IS NULL OR is_public_profile = 0", + (updateErr) => { + if (updateErr) { + console.error( + "Ошибка обновления is_public_profile для существующих пользователей:", + updateErr.message + ); + } else { + console.log( + "is_public_profile установлен в 1 для всех существующих пользователей" + ); } } ); @@ -642,6 +675,7 @@ function runMigrations() { const hasPinned = columns.some((col) => col.name === "is_pinned"); const hasArchived = columns.some((col) => col.name === "is_archived"); const hasPinnedAt = columns.some((col) => col.name === "pinned_at"); + const hasIsPrivate = columns.some((col) => col.name === "is_private"); // Добавляем updated_at если нужно if (!hasUpdatedAt) { @@ -707,6 +741,20 @@ function runMigrations() { }); } + // Добавляем is_private если нужно + if (!hasIsPrivate) { + db.run( + "ALTER TABLE notes ADD COLUMN is_private INTEGER DEFAULT 1", + (err) => { + if (err) { + console.error("Ошибка добавления колонки is_private:", err.message); + } else { + console.log("Колонка is_private добавлена в таблицу notes"); + } + } + ); + } + // Создаем индексы после всех изменений if (hasUpdatedAt && hasPinned && hasArchived) { createIndexes(); @@ -782,8 +830,8 @@ app.post("/api/register", async (req, res) => { // Хешируем пароль const hashedPassword = await bcrypt.hash(password, 10); - // Вставляем пользователя в БД - const sql = "INSERT INTO users (username, password) VALUES (?, ?)"; + // Вставляем пользователя в БД (is_public_profile по умолчанию 1) + const sql = "INSERT INTO users (username, password, is_public_profile) VALUES (?, ?, 1)"; db.run(sql, [username, hashedPassword], function (err) { if (err) { if (err.message.includes("UNIQUE constraint failed")) { @@ -1094,7 +1142,7 @@ app.get("/api/public/user/:username/notes", (req, res) => { FROM notes n LEFT JOIN note_images ni ON n.id = ni.note_id LEFT JOIN note_files nf ON n.id = nf.note_id - WHERE n.user_id = ? AND n.is_archived = 0 + WHERE n.user_id = ? AND n.is_archived = 0 AND (n.is_private = 0 OR n.is_private IS NULL) GROUP BY n.id ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC `; @@ -1346,7 +1394,7 @@ app.get("/api/notes", requireApiAuth, (req, res) => { // API для создания новой заметки app.post("/api/notes", requireApiAuth, (req, res) => { - const { content, date, time } = req.body; + const { content, date, time, is_private } = req.body; if (!content || !date || !time) { return res.status(400).json({ error: "Не все поля заполнены" }); @@ -1355,9 +1403,12 @@ app.post("/api/notes", requireApiAuth, (req, res) => { // Шифруем содержимое заметки перед сохранением const encryptedContent = encrypt(content); + // По умолчанию заметка приватная (is_private = 1) + const isPrivateValue = is_private !== undefined ? is_private : 1; + const sql = - "INSERT INTO notes (user_id, content, date, time, created_at, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"; - const params = [req.session.userId, encryptedContent, date, time]; + "INSERT INTO notes (user_id, content, date, time, is_private, created_at, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"; + const params = [req.session.userId, encryptedContent, date, time, isPrivateValue]; db.run(sql, params, function (err) { if (err) { @@ -1375,7 +1426,7 @@ app.post("/api/notes", requireApiAuth, (req, res) => { // API для обновления заметки app.put("/api/notes/:id", requireApiAuth, (req, res) => { - const { content, skipTimestamp } = req.body; + const { content, skipTimestamp, is_private } = req.body; const { id } = req.params; if (!content) { @@ -1402,10 +1453,24 @@ app.put("/api/notes/:id", requireApiAuth, (req, res) => { } // Если skipTimestamp=true (обновление чекбокса), не обновляем updated_at - const updateSql = skipTimestamp - ? "UPDATE notes SET content = ? WHERE id = ?" - : "UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"; - const params = [encryptedContent, id]; + let updateSql; + let params; + + if (is_private !== undefined) { + // Обновляем и content, и is_private + updateSql = skipTimestamp + ? "UPDATE notes SET content = ?, is_private = ? WHERE id = ?" + : "UPDATE notes SET content = ?, is_private = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"; + params = skipTimestamp + ? [encryptedContent, is_private, id] + : [encryptedContent, is_private, id]; + } else { + // Обновляем только content + updateSql = skipTimestamp + ? "UPDATE notes SET content = ? WHERE id = ?" + : "UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?"; + params = [encryptedContent, id]; + } db.run(updateSql, params, function (err) { if (err) { diff --git a/dev-dist/sw.js b/dev-dist/sw.js index a8f9061..8806e80 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-47da91e0'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "/index.html", - "revision": "0.kpjpajqk6vo" + "revision": "0.umqqd7sqgfg" }], { "ignoreURLParametersMatching": [/^utm_/, /^fbclid$/] }); diff --git a/src/api/notesApi.ts b/src/api/notesApi.ts index adad280..bcb4b46 100644 --- a/src/api/notesApi.ts +++ b/src/api/notesApi.ts @@ -16,15 +16,16 @@ export const notesApi = { return data; }, - create: async (note: { content: string; date: string; time: string }) => { + create: async (note: { content: string; date: string; time: string; is_private?: 0 | 1 }) => { const { data } = await axiosClient.post("/notes", note); return data; }, - update: async (id: number, content: string, skipTimestamp?: boolean) => { + update: async (id: number, content: string, skipTimestamp?: boolean, is_private?: 0 | 1) => { const { data } = await axiosClient.put(`/notes/${id}`, { content, skipTimestamp, + is_private, }); return data; }, diff --git a/src/api/offlineNotesApi.ts b/src/api/offlineNotesApi.ts index 125d121..36f62d8 100644 --- a/src/api/offlineNotesApi.ts +++ b/src/api/offlineNotesApi.ts @@ -180,6 +180,7 @@ export const offlineNotesApi = { content: string; date: string; time: string; + is_private?: 0 | 1; }): Promise => { const online = await isOnline(); const userId = getUserId(); @@ -197,6 +198,7 @@ export const offlineNotesApi = { updated_at: now, is_pinned: 0, is_archived: 0, + is_private: note.is_private !== undefined ? note.is_private : 1, images: [], files: [], syncStatus: "pending", @@ -268,6 +270,7 @@ export const offlineNotesApi = { updated_at: now, is_pinned: 0, is_archived: 0, + is_private: note.is_private !== undefined ? note.is_private : 1, images: [], files: [], syncStatus: "pending", @@ -304,7 +307,8 @@ export const offlineNotesApi = { update: async ( id: number | string, content: string, - skipTimestamp?: boolean + skipTimestamp?: boolean, + is_private?: 0 | 1 ): Promise => { const online = await isOnline(); @@ -319,6 +323,7 @@ export const offlineNotesApi = { const updatedNote: Note = { ...existingNote, content, + is_private: is_private !== undefined ? is_private : existingNote.is_private, updated_at: new Date().toISOString(), syncStatus: "pending", }; @@ -329,7 +334,7 @@ export const offlineNotesApi = { await dbManager.addToSyncQueue({ type: "update", noteId: id, - data: { content, skipTimestamp }, + data: { content, skipTimestamp, is_private }, timestamp: Date.now(), retries: 0, }); @@ -350,6 +355,7 @@ export const offlineNotesApi = { const { data } = await axiosClient.put(`/notes/${id}`, { content, skipTimestamp, + is_private, }); const noteWithSyncStatus = { diff --git a/src/components/notes/NoteEditor.tsx b/src/components/notes/NoteEditor.tsx index 95093c4..e3d343a 100644 --- a/src/components/notes/NoteEditor.tsx +++ b/src/components/notes/NoteEditor.tsx @@ -21,6 +21,7 @@ export const NoteEditor: React.FC = ({ onSave }) => { const [content, setContent] = useState(""); const [images, setImages] = useState([]); const [files, setFiles] = useState([]); + const [isPrivate, setIsPrivate] = useState(true); // По умолчанию включен (приватная) const [isAiLoading, setIsAiLoading] = useState(false); const [showTagsModal, setShowTagsModal] = useState(false); const [suggestedTags, setSuggestedTags] = useState([]); @@ -64,7 +65,7 @@ export const NoteEditor: React.FC = ({ onSave }) => { minute: "2-digit", }); - const note = await offlineNotesApi.create({ content, date, time }); + const note = await offlineNotesApi.create({ content, date, time, is_private: isPrivate ? 1 : 0 }); // Загружаем изображения if (images.length > 0) { @@ -80,6 +81,7 @@ export const NoteEditor: React.FC = ({ onSave }) => { setContent(""); setImages([]); setFiles([]); + setIsPrivate(true); // Сбрасываем тумблер к значению по умолчанию onSave(note.id); } catch (error) { console.error("Ошибка сохранения заметки:", error); @@ -1078,6 +1080,22 @@ export const NoteEditor: React.FC = ({ onSave }) => { + {user?.is_public_profile === 1 && ( +
+ +
+ )} +
{aiEnabled && ( diff --git a/src/components/notes/NoteItem.tsx b/src/components/notes/NoteItem.tsx index 4be0d14..6cfef4c 100644 --- a/src/components/notes/NoteItem.tsx +++ b/src/components/notes/NoteItem.tsx @@ -47,10 +47,15 @@ export const NoteItem: React.FC = ({ }) => { const [isEditing, setIsEditing] = useState(false); const [editContent, setEditContent] = useState(note.content); + const [editIsPrivate, setEditIsPrivate] = useState( + note.is_private !== undefined ? note.is_private === 1 : true + ); const [showArchiveModal, setShowArchiveModal] = useState(false); const [editImages, setEditImages] = useState([]); const [editFiles, setEditFiles] = useState([]); - const [deletedImageIds, setDeletedImageIds] = useState<(number | string)[]>([]); + const [deletedImageIds, setDeletedImageIds] = useState<(number | string)[]>( + [] + ); const [deletedFileIds, setDeletedFileIds] = useState<(number | string)[]>([]); const [isAiLoading, setIsAiLoading] = useState(false); const [showFloatingToolbar, setShowFloatingToolbar] = useState(false); @@ -84,7 +89,7 @@ export const NoteItem: React.FC = ({ const { showNotification } = useNotification(); const dispatch = useAppDispatch(); useMarkdown({ onNoteUpdate: onReload }); // Инициализируем обработчики спойлеров, внешних ссылок и чекбоксов - + // Проверяем, включена ли плавающая панель const floatingToolbarEnabled = user?.floating_toolbar_enabled !== undefined @@ -94,6 +99,9 @@ export const NoteItem: React.FC = ({ const handleEdit = () => { setIsEditing(true); setEditContent(note.content); + setEditIsPrivate( + note.is_private !== undefined ? note.is_private === 1 : true + ); setEditImages([]); setEditFiles([]); setDeletedImageIds([]); @@ -116,7 +124,12 @@ export const NoteItem: React.FC = ({ } try { - await offlineNotesApi.update(note.id, editContent); + await offlineNotesApi.update( + note.id, + editContent, + false, + editIsPrivate ? 1 : 0 + ); // Удаляем выбранные изображения for (const imageId of deletedImageIds) { @@ -161,6 +174,9 @@ export const NoteItem: React.FC = ({ const handleCancelEdit = () => { setIsEditing(false); setEditContent(note.content); + setEditIsPrivate( + note.is_private !== undefined ? note.is_private === 1 : true + ); setEditImages([]); setEditFiles([]); setDeletedImageIds([]); @@ -248,7 +264,10 @@ export const NoteItem: React.FC = ({ console.error("Детали ошибки:", error.response?.data); setTagsGenerationError(true); setShowTagsModal(false); - const errorMessage = error.response?.data?.error || error.message || "Ошибка генерации тегов"; + const errorMessage = + error.response?.data?.error || + error.message || + "Ошибка генерации тегов"; showNotification(errorMessage, "error"); } finally { setIsGeneratingTags(false); @@ -260,13 +279,19 @@ export const NoteItem: React.FC = ({ const existingTags = extractTags(editContent); const tagsToAdd = tags - .filter((tag) => !existingTags.some((existing) => existing.toLowerCase() === tag.toLowerCase())) + .filter( + (tag) => + !existingTags.some( + (existing) => existing.toLowerCase() === tag.toLowerCase() + ) + ) .map((tag) => `#${tag}`) .join(" "); if (tagsToAdd) { // Добавляем теги в конец заметки - const newContent = editContent.trim() + (editContent.trim() ? "\n\n" : "") + tagsToAdd; + const newContent = + editContent.trim() + (editContent.trim() ? "\n\n" : "") + tagsToAdd; setEditContent(newContent); showNotification(`Добавлено тегов: ${tags.length}`, "success"); } else { @@ -516,7 +541,9 @@ export const NoteItem: React.FC = ({ } } else { // Проверяем, является ли это форматированием списка или цитаты - const isListFormatting = /^[-*+]\s|^\d+\.\s|^- \[ \]\s|^>\s/.test(before); + const isListFormatting = /^[-*+]\s|^\d+\.\s|^- \[ \]\s|^>\s/.test( + before + ); const isMultiline = selectedText.includes("\n"); if (isListFormatting && isMultiline) { @@ -529,7 +556,7 @@ export const NoteItem: React.FC = ({ for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); - + // Пропускаем пустые строки if (trimmedLine === "") { processedLines.push(line); @@ -719,7 +746,13 @@ export const NoteItem: React.FC = ({ setHasSelection(false); setActiveFormats({ bold: false, italic: false, strikethrough: false }); } - }, [localPreviewMode, editContent, getCursorPosition, getActiveFormats, floatingToolbarEnabled]); + }, [ + localPreviewMode, + editContent, + getCursorPosition, + getActiveFormats, + floatingToolbarEnabled, + ]); const handleImageButtonClick = () => { imageInputRef.current?.click(); @@ -1083,7 +1116,8 @@ export const NoteItem: React.FC = ({ ); if (note.updated_at && note.created_at !== note.updated_at) { - const showEditDate = user?.show_edit_date !== undefined ? user.show_edit_date === 1 : true; + const showEditDate = + user?.show_edit_date !== undefined ? user.show_edit_date === 1 : true; const updated = parseSQLiteUtc(note.updated_at); const updatedStr = formatLocalDateTime(updated); // Убеждаемся, что строка не содержит лишних символов @@ -1091,7 +1125,7 @@ export const NoteItem: React.FC = ({ /(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})\d*.*/, "$1" ); - + if (showEditDate) { return ( <> @@ -1228,13 +1262,16 @@ export const NoteItem: React.FC = ({ Закреплено ) : null} - {note.syncStatus === 'pending' && ( + {note.syncStatus === "pending" && ( )} - {note.syncStatus === 'error' && ( - + {note.syncStatus === "error" && ( + )} @@ -1507,6 +1544,42 @@ export const NoteItem: React.FC = ({ + {user?.is_public_profile === 1 && ( +
+ +
+ )} +
{aiEnabled && ( @@ -1607,7 +1680,11 @@ export const NoteItem: React.FC = ({ {note.files && note.files.length > 0 && (