Добавлен функционал для управления приватностью заметок. Реализованы изменения в базе данных для добавления поля is_private, обновлены API и компоненты для поддержки создания и редактирования заметок с учетом приватности. По умолчанию заметки создаются как приватные, а пользователи могут изменять настройки приватности через интерфейс. Обновлены типы данных для поддержки новых функций.
This commit is contained in:
parent
160ae68d1e
commit
91c6f46fb4
@ -614,7 +614,7 @@ function runMigrations() {
|
|||||||
);
|
);
|
||||||
if (!hasIsPublicProfile) {
|
if (!hasIsPublicProfile) {
|
||||||
db.run(
|
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) => {
|
(err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(
|
console.error(
|
||||||
@ -625,6 +625,39 @@ function runMigrations() {
|
|||||||
console.log(
|
console.log(
|
||||||
"Колонка is_public_profile добавлена в таблицу users"
|
"Колонка 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 hasPinned = columns.some((col) => col.name === "is_pinned");
|
||||||
const hasArchived = columns.some((col) => col.name === "is_archived");
|
const hasArchived = columns.some((col) => col.name === "is_archived");
|
||||||
const hasPinnedAt = columns.some((col) => col.name === "pinned_at");
|
const hasPinnedAt = columns.some((col) => col.name === "pinned_at");
|
||||||
|
const hasIsPrivate = columns.some((col) => col.name === "is_private");
|
||||||
|
|
||||||
// Добавляем updated_at если нужно
|
// Добавляем updated_at если нужно
|
||||||
if (!hasUpdatedAt) {
|
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) {
|
if (hasUpdatedAt && hasPinned && hasArchived) {
|
||||||
createIndexes();
|
createIndexes();
|
||||||
@ -782,8 +830,8 @@ app.post("/api/register", async (req, res) => {
|
|||||||
// Хешируем пароль
|
// Хешируем пароль
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
// Вставляем пользователя в БД
|
// Вставляем пользователя в БД (is_public_profile по умолчанию 1)
|
||||||
const sql = "INSERT INTO users (username, password) VALUES (?, ?)";
|
const sql = "INSERT INTO users (username, password, is_public_profile) VALUES (?, ?, 1)";
|
||||||
db.run(sql, [username, hashedPassword], function (err) {
|
db.run(sql, [username, hashedPassword], function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
if (err.message.includes("UNIQUE constraint failed")) {
|
if (err.message.includes("UNIQUE constraint failed")) {
|
||||||
@ -1094,7 +1142,7 @@ app.get("/api/public/user/:username/notes", (req, res) => {
|
|||||||
FROM notes n
|
FROM notes n
|
||||||
LEFT JOIN note_images ni ON n.id = ni.note_id
|
LEFT JOIN note_images ni ON n.id = ni.note_id
|
||||||
LEFT JOIN note_files nf ON n.id = nf.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
|
GROUP BY n.id
|
||||||
ORDER BY n.is_pinned DESC, n.pinned_at DESC, n.created_at DESC
|
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 для создания новой заметки
|
// API для создания новой заметки
|
||||||
app.post("/api/notes", requireApiAuth, (req, res) => {
|
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) {
|
if (!content || !date || !time) {
|
||||||
return res.status(400).json({ error: "Не все поля заполнены" });
|
return res.status(400).json({ error: "Не все поля заполнены" });
|
||||||
@ -1355,9 +1403,12 @@ app.post("/api/notes", requireApiAuth, (req, res) => {
|
|||||||
// Шифруем содержимое заметки перед сохранением
|
// Шифруем содержимое заметки перед сохранением
|
||||||
const encryptedContent = encrypt(content);
|
const encryptedContent = encrypt(content);
|
||||||
|
|
||||||
|
// По умолчанию заметка приватная (is_private = 1)
|
||||||
|
const isPrivateValue = is_private !== undefined ? is_private : 1;
|
||||||
|
|
||||||
const sql =
|
const sql =
|
||||||
"INSERT INTO notes (user_id, content, date, time, created_at, updated_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)";
|
"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];
|
const params = [req.session.userId, encryptedContent, date, time, isPrivateValue];
|
||||||
|
|
||||||
db.run(sql, params, function (err) {
|
db.run(sql, params, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -1375,7 +1426,7 @@ app.post("/api/notes", requireApiAuth, (req, res) => {
|
|||||||
|
|
||||||
// API для обновления заметки
|
// API для обновления заметки
|
||||||
app.put("/api/notes/:id", requireApiAuth, (req, res) => {
|
app.put("/api/notes/:id", requireApiAuth, (req, res) => {
|
||||||
const { content, skipTimestamp } = req.body;
|
const { content, skipTimestamp, is_private } = req.body;
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
@ -1402,10 +1453,24 @@ app.put("/api/notes/:id", requireApiAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Если skipTimestamp=true (обновление чекбокса), не обновляем updated_at
|
// Если skipTimestamp=true (обновление чекбокса), не обновляем updated_at
|
||||||
const updateSql = skipTimestamp
|
let updateSql;
|
||||||
? "UPDATE notes SET content = ? WHERE id = ?"
|
let params;
|
||||||
: "UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
|
|
||||||
const params = [encryptedContent, id];
|
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) {
|
db.run(updateSql, params, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@ -82,7 +82,7 @@ define(['./workbox-47da91e0'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "/index.html",
|
"url": "/index.html",
|
||||||
"revision": "0.kpjpajqk6vo"
|
"revision": "0.umqqd7sqgfg"
|
||||||
}], {
|
}], {
|
||||||
"ignoreURLParametersMatching": [/^utm_/, /^fbclid$/]
|
"ignoreURLParametersMatching": [/^utm_/, /^fbclid$/]
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,15 +16,16 @@ export const notesApi = {
|
|||||||
return data;
|
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);
|
const { data } = await axiosClient.post("/notes", note);
|
||||||
return data;
|
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}`, {
|
const { data } = await axiosClient.put(`/notes/${id}`, {
|
||||||
content,
|
content,
|
||||||
skipTimestamp,
|
skipTimestamp,
|
||||||
|
is_private,
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -180,6 +180,7 @@ export const offlineNotesApi = {
|
|||||||
content: string;
|
content: string;
|
||||||
date: string;
|
date: string;
|
||||||
time: string;
|
time: string;
|
||||||
|
is_private?: 0 | 1;
|
||||||
}): Promise<Note> => {
|
}): Promise<Note> => {
|
||||||
const online = await isOnline();
|
const online = await isOnline();
|
||||||
const userId = getUserId();
|
const userId = getUserId();
|
||||||
@ -197,6 +198,7 @@ export const offlineNotesApi = {
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
is_pinned: 0,
|
is_pinned: 0,
|
||||||
is_archived: 0,
|
is_archived: 0,
|
||||||
|
is_private: note.is_private !== undefined ? note.is_private : 1,
|
||||||
images: [],
|
images: [],
|
||||||
files: [],
|
files: [],
|
||||||
syncStatus: "pending",
|
syncStatus: "pending",
|
||||||
@ -268,6 +270,7 @@ export const offlineNotesApi = {
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
is_pinned: 0,
|
is_pinned: 0,
|
||||||
is_archived: 0,
|
is_archived: 0,
|
||||||
|
is_private: note.is_private !== undefined ? note.is_private : 1,
|
||||||
images: [],
|
images: [],
|
||||||
files: [],
|
files: [],
|
||||||
syncStatus: "pending",
|
syncStatus: "pending",
|
||||||
@ -304,7 +307,8 @@ export const offlineNotesApi = {
|
|||||||
update: async (
|
update: async (
|
||||||
id: number | string,
|
id: number | string,
|
||||||
content: string,
|
content: string,
|
||||||
skipTimestamp?: boolean
|
skipTimestamp?: boolean,
|
||||||
|
is_private?: 0 | 1
|
||||||
): Promise<Note> => {
|
): Promise<Note> => {
|
||||||
const online = await isOnline();
|
const online = await isOnline();
|
||||||
|
|
||||||
@ -319,6 +323,7 @@ export const offlineNotesApi = {
|
|||||||
const updatedNote: Note = {
|
const updatedNote: Note = {
|
||||||
...existingNote,
|
...existingNote,
|
||||||
content,
|
content,
|
||||||
|
is_private: is_private !== undefined ? is_private : existingNote.is_private,
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
syncStatus: "pending",
|
syncStatus: "pending",
|
||||||
};
|
};
|
||||||
@ -329,7 +334,7 @@ export const offlineNotesApi = {
|
|||||||
await dbManager.addToSyncQueue({
|
await dbManager.addToSyncQueue({
|
||||||
type: "update",
|
type: "update",
|
||||||
noteId: id,
|
noteId: id,
|
||||||
data: { content, skipTimestamp },
|
data: { content, skipTimestamp, is_private },
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
retries: 0,
|
retries: 0,
|
||||||
});
|
});
|
||||||
@ -350,6 +355,7 @@ export const offlineNotesApi = {
|
|||||||
const { data } = await axiosClient.put(`/notes/${id}`, {
|
const { data } = await axiosClient.put(`/notes/${id}`, {
|
||||||
content,
|
content,
|
||||||
skipTimestamp,
|
skipTimestamp,
|
||||||
|
is_private,
|
||||||
});
|
});
|
||||||
|
|
||||||
const noteWithSyncStatus = {
|
const noteWithSyncStatus = {
|
||||||
|
|||||||
@ -21,6 +21,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [images, setImages] = useState<File[]>([]);
|
const [images, setImages] = useState<File[]>([]);
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
const [isPrivate, setIsPrivate] = useState(true); // По умолчанию включен (приватная)
|
||||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||||
const [showTagsModal, setShowTagsModal] = useState(false);
|
const [showTagsModal, setShowTagsModal] = useState(false);
|
||||||
const [suggestedTags, setSuggestedTags] = useState<string[]>([]);
|
const [suggestedTags, setSuggestedTags] = useState<string[]>([]);
|
||||||
@ -64,7 +65,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
minute: "2-digit",
|
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) {
|
if (images.length > 0) {
|
||||||
@ -80,6 +81,7 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
setContent("");
|
setContent("");
|
||||||
setImages([]);
|
setImages([]);
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
|
setIsPrivate(true); // Сбрасываем тумблер к значению по умолчанию
|
||||||
onSave(note.id);
|
onSave(note.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Ошибка сохранения заметки:", error);
|
console.error("Ошибка сохранения заметки:", error);
|
||||||
@ -1078,6 +1080,22 @@ export const NoteEditor: React.FC<NoteEditorProps> = ({ onSave }) => {
|
|||||||
<ImageUpload images={images} onChange={setImages} />
|
<ImageUpload images={images} onChange={setImages} />
|
||||||
<FileUpload files={files} onChange={setFiles} />
|
<FileUpload files={files} onChange={setFiles} />
|
||||||
|
|
||||||
|
{user?.is_public_profile === 1 && (
|
||||||
|
<div className="privacy-toggle-container" style={{ marginBottom: "10px", display: "flex", alignItems: "center", gap: "10px" }}>
|
||||||
|
<label style={{ display: "flex", alignItems: "center", gap: "8px", cursor: "pointer" }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isPrivate}
|
||||||
|
onChange={(e) => setIsPrivate(e.target.checked)}
|
||||||
|
style={{ width: "18px", height: "18px", cursor: "pointer" }}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: "14px", color: "var(--text-primary, #333)" }}>
|
||||||
|
Приватная заметка
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="save-button-container">
|
<div className="save-button-container">
|
||||||
<div className="action-buttons">
|
<div className="action-buttons">
|
||||||
{aiEnabled && (
|
{aiEnabled && (
|
||||||
|
|||||||
@ -47,10 +47,15 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [editContent, setEditContent] = useState(note.content);
|
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 [showArchiveModal, setShowArchiveModal] = useState(false);
|
||||||
const [editImages, setEditImages] = useState<File[]>([]);
|
const [editImages, setEditImages] = useState<File[]>([]);
|
||||||
const [editFiles, setEditFiles] = useState<File[]>([]);
|
const [editFiles, setEditFiles] = useState<File[]>([]);
|
||||||
const [deletedImageIds, setDeletedImageIds] = useState<(number | string)[]>([]);
|
const [deletedImageIds, setDeletedImageIds] = useState<(number | string)[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
const [deletedFileIds, setDeletedFileIds] = useState<(number | string)[]>([]);
|
const [deletedFileIds, setDeletedFileIds] = useState<(number | string)[]>([]);
|
||||||
const [isAiLoading, setIsAiLoading] = useState(false);
|
const [isAiLoading, setIsAiLoading] = useState(false);
|
||||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||||
@ -84,7 +89,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
useMarkdown({ onNoteUpdate: onReload }); // Инициализируем обработчики спойлеров, внешних ссылок и чекбоксов
|
useMarkdown({ onNoteUpdate: onReload }); // Инициализируем обработчики спойлеров, внешних ссылок и чекбоксов
|
||||||
|
|
||||||
// Проверяем, включена ли плавающая панель
|
// Проверяем, включена ли плавающая панель
|
||||||
const floatingToolbarEnabled =
|
const floatingToolbarEnabled =
|
||||||
user?.floating_toolbar_enabled !== undefined
|
user?.floating_toolbar_enabled !== undefined
|
||||||
@ -94,6 +99,9 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
setIsEditing(true);
|
setIsEditing(true);
|
||||||
setEditContent(note.content);
|
setEditContent(note.content);
|
||||||
|
setEditIsPrivate(
|
||||||
|
note.is_private !== undefined ? note.is_private === 1 : true
|
||||||
|
);
|
||||||
setEditImages([]);
|
setEditImages([]);
|
||||||
setEditFiles([]);
|
setEditFiles([]);
|
||||||
setDeletedImageIds([]);
|
setDeletedImageIds([]);
|
||||||
@ -116,7 +124,12 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await offlineNotesApi.update(note.id, editContent);
|
await offlineNotesApi.update(
|
||||||
|
note.id,
|
||||||
|
editContent,
|
||||||
|
false,
|
||||||
|
editIsPrivate ? 1 : 0
|
||||||
|
);
|
||||||
|
|
||||||
// Удаляем выбранные изображения
|
// Удаляем выбранные изображения
|
||||||
for (const imageId of deletedImageIds) {
|
for (const imageId of deletedImageIds) {
|
||||||
@ -161,6 +174,9 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
const handleCancelEdit = () => {
|
const handleCancelEdit = () => {
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
setEditContent(note.content);
|
setEditContent(note.content);
|
||||||
|
setEditIsPrivate(
|
||||||
|
note.is_private !== undefined ? note.is_private === 1 : true
|
||||||
|
);
|
||||||
setEditImages([]);
|
setEditImages([]);
|
||||||
setEditFiles([]);
|
setEditFiles([]);
|
||||||
setDeletedImageIds([]);
|
setDeletedImageIds([]);
|
||||||
@ -248,7 +264,10 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
console.error("Детали ошибки:", error.response?.data);
|
console.error("Детали ошибки:", error.response?.data);
|
||||||
setTagsGenerationError(true);
|
setTagsGenerationError(true);
|
||||||
setShowTagsModal(false);
|
setShowTagsModal(false);
|
||||||
const errorMessage = error.response?.data?.error || error.message || "Ошибка генерации тегов";
|
const errorMessage =
|
||||||
|
error.response?.data?.error ||
|
||||||
|
error.message ||
|
||||||
|
"Ошибка генерации тегов";
|
||||||
showNotification(errorMessage, "error");
|
showNotification(errorMessage, "error");
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingTags(false);
|
setIsGeneratingTags(false);
|
||||||
@ -260,13 +279,19 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
|
|
||||||
const existingTags = extractTags(editContent);
|
const existingTags = extractTags(editContent);
|
||||||
const tagsToAdd = tags
|
const tagsToAdd = tags
|
||||||
.filter((tag) => !existingTags.some((existing) => existing.toLowerCase() === tag.toLowerCase()))
|
.filter(
|
||||||
|
(tag) =>
|
||||||
|
!existingTags.some(
|
||||||
|
(existing) => existing.toLowerCase() === tag.toLowerCase()
|
||||||
|
)
|
||||||
|
)
|
||||||
.map((tag) => `#${tag}`)
|
.map((tag) => `#${tag}`)
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
if (tagsToAdd) {
|
if (tagsToAdd) {
|
||||||
// Добавляем теги в конец заметки
|
// Добавляем теги в конец заметки
|
||||||
const newContent = editContent.trim() + (editContent.trim() ? "\n\n" : "") + tagsToAdd;
|
const newContent =
|
||||||
|
editContent.trim() + (editContent.trim() ? "\n\n" : "") + tagsToAdd;
|
||||||
setEditContent(newContent);
|
setEditContent(newContent);
|
||||||
showNotification(`Добавлено тегов: ${tags.length}`, "success");
|
showNotification(`Добавлено тегов: ${tags.length}`, "success");
|
||||||
} else {
|
} else {
|
||||||
@ -516,7 +541,9 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Проверяем, является ли это форматированием списка или цитаты
|
// Проверяем, является ли это форматированием списка или цитаты
|
||||||
const isListFormatting = /^[-*+]\s|^\d+\.\s|^- \[ \]\s|^>\s/.test(before);
|
const isListFormatting = /^[-*+]\s|^\d+\.\s|^- \[ \]\s|^>\s/.test(
|
||||||
|
before
|
||||||
|
);
|
||||||
const isMultiline = selectedText.includes("\n");
|
const isMultiline = selectedText.includes("\n");
|
||||||
|
|
||||||
if (isListFormatting && isMultiline) {
|
if (isListFormatting && isMultiline) {
|
||||||
@ -529,7 +556,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
const trimmedLine = line.trim();
|
const trimmedLine = line.trim();
|
||||||
|
|
||||||
// Пропускаем пустые строки
|
// Пропускаем пустые строки
|
||||||
if (trimmedLine === "") {
|
if (trimmedLine === "") {
|
||||||
processedLines.push(line);
|
processedLines.push(line);
|
||||||
@ -719,7 +746,13 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
setHasSelection(false);
|
setHasSelection(false);
|
||||||
setActiveFormats({ bold: false, italic: false, strikethrough: false });
|
setActiveFormats({ bold: false, italic: false, strikethrough: false });
|
||||||
}
|
}
|
||||||
}, [localPreviewMode, editContent, getCursorPosition, getActiveFormats, floatingToolbarEnabled]);
|
}, [
|
||||||
|
localPreviewMode,
|
||||||
|
editContent,
|
||||||
|
getCursorPosition,
|
||||||
|
getActiveFormats,
|
||||||
|
floatingToolbarEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleImageButtonClick = () => {
|
const handleImageButtonClick = () => {
|
||||||
imageInputRef.current?.click();
|
imageInputRef.current?.click();
|
||||||
@ -1083,7 +1116,8 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (note.updated_at && note.created_at !== note.updated_at) {
|
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 updated = parseSQLiteUtc(note.updated_at);
|
||||||
const updatedStr = formatLocalDateTime(updated);
|
const updatedStr = formatLocalDateTime(updated);
|
||||||
// Убеждаемся, что строка не содержит лишних символов
|
// Убеждаемся, что строка не содержит лишних символов
|
||||||
@ -1091,7 +1125,7 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
/(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})\d*.*/,
|
/(\d{2}\.\d{2}\.\d{4} \d{2}:\d{2})\d*.*/,
|
||||||
"$1"
|
"$1"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (showEditDate) {
|
if (showEditDate) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -1228,13 +1262,16 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
Закреплено
|
Закреплено
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{note.syncStatus === 'pending' && (
|
{note.syncStatus === "pending" && (
|
||||||
<span className="sync-indicator" title="Ожидает синхронизации">
|
<span className="sync-indicator" title="Ожидает синхронизации">
|
||||||
<Icon icon="mdi:cloud-upload" />
|
<Icon icon="mdi:cloud-upload" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{note.syncStatus === 'error' && (
|
{note.syncStatus === "error" && (
|
||||||
<span className="sync-error-indicator" title="Ошибка синхронизации">
|
<span
|
||||||
|
className="sync-error-indicator"
|
||||||
|
title="Ошибка синхронизации"
|
||||||
|
>
|
||||||
<Icon icon="mdi:cloud-alert" />
|
<Icon icon="mdi:cloud-alert" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@ -1507,6 +1544,42 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
<ImageUpload images={editImages} onChange={setEditImages} />
|
<ImageUpload images={editImages} onChange={setEditImages} />
|
||||||
<FileUpload files={editFiles} onChange={setEditFiles} />
|
<FileUpload files={editFiles} onChange={setEditFiles} />
|
||||||
|
|
||||||
|
{user?.is_public_profile === 1 && (
|
||||||
|
<div
|
||||||
|
className="privacy-toggle-container"
|
||||||
|
style={{
|
||||||
|
marginBottom: "10px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "8px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editIsPrivate}
|
||||||
|
onChange={(e) => setEditIsPrivate(e.target.checked)}
|
||||||
|
style={{ width: "18px", height: "18px", cursor: "pointer" }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "var(--text-primary, #333)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Приватная заметка
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="save-button-container">
|
<div className="save-button-container">
|
||||||
<div className="action-buttons">
|
<div className="action-buttons">
|
||||||
{aiEnabled && (
|
{aiEnabled && (
|
||||||
@ -1607,7 +1680,11 @@ export const NoteItem: React.FC<NoteItemProps> = ({
|
|||||||
{note.files && note.files.length > 0 && (
|
{note.files && note.files.length > 0 && (
|
||||||
<div className="note-files-container">
|
<div className="note-files-container">
|
||||||
{note.files.map((file) => {
|
{note.files.map((file) => {
|
||||||
const fileUrl = getFileUrl(file.file_path, Number(note.id), Number(file.id));
|
const fileUrl = getFileUrl(
|
||||||
|
file.file_path,
|
||||||
|
Number(note.id),
|
||||||
|
Number(file.id)
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div key={file.id} className="note-file-item">
|
<div key={file.id} className="note-file-item">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@ -248,6 +248,7 @@ class SyncService {
|
|||||||
content: note.content,
|
content: note.content,
|
||||||
date: note.date,
|
date: note.date,
|
||||||
time: note.time,
|
time: note.time,
|
||||||
|
is_private: note.is_private !== undefined ? note.is_private : 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Сохраняем маппинг temp ID -> server ID
|
// Сохраняем маппинг temp ID -> server ID
|
||||||
@ -309,6 +310,7 @@ class SyncService {
|
|||||||
await axiosClient.put(`/notes/${item.noteId}`, {
|
await axiosClient.put(`/notes/${item.noteId}`, {
|
||||||
content: note.content,
|
content: note.content,
|
||||||
skipTimestamp: item.data.skipTimestamp,
|
skipTimestamp: item.data.skipTimestamp,
|
||||||
|
is_private: item.data.is_private !== undefined ? item.data.is_private : note.is_private,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export interface Note {
|
|||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
is_pinned: 0 | 1;
|
is_pinned: 0 | 1;
|
||||||
is_archived: 0 | 1;
|
is_archived: 0 | 1;
|
||||||
|
is_private?: 0 | 1;
|
||||||
pinned_at?: string;
|
pinned_at?: string;
|
||||||
images: NoteImage[];
|
images: NoteImage[];
|
||||||
files: NoteFile[];
|
files: NoteFile[];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user